diff --git a/CHANGELOG.md b/CHANGELOG.md index efa47466aa..588edf3067 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.46.5] - 2025-01-17 +### Fixed +- Removed the API token error log message that was being printed when the API token was invalid. + +## [2.46.4] - 2024-06-24 +### Fixed +- Check for already existing Namespace and Name when renaming a repository +- Repositories with the same namespace and name will get unique names on startup + +## [2.46.3] - 2024-05-29 +### Fixed +- Exception in SVN repositories due to incorrect git initialization (Backport from 2.47) +- Default branch evaluation on git config initialization + +## [2.46.2] - 2024-03-04 +### Fixed +- Rendering PDF files in source view + ## [2.48.3] - 2023-12-08 ### Fixed - Removed function `toSpliced` due to missing browser support @@ -1438,6 +1456,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [2.45.1]: https://scm-manager.org/download/2.45.1 [2.46.0]: https://scm-manager.org/download/2.46.0 [2.46.1]: https://scm-manager.org/download/2.46.1 +[2.46.2]: https://scm-manager.org/download/2.46.2 +[2.46.3]: https://scm-manager.org/download/2.46.3 +[2.46.4]: https://scm-manager.org/download/2.46.4 +[2.46.5]: https://scm-manager.org/download/2.46.5 [2.47.0]: https://scm-manager.org/download/2.47.0 [2.48.0]: https://scm-manager.org/download/2.48.0 [2.48.1]: https://scm-manager.org/download/2.48.1 diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java index 7df734cd8e..217e24dd30 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java @@ -105,6 +105,10 @@ public class XmlRepositoryDAO implements RepositoryDAO { pathRepositoryLocationResolverInstance.forAllLocations((repositoryId, repositoryPath) -> { try { Repository repository = metadataStore.read(repositoryPath); + if (byNamespaceAndName.containsKey(repository.getNamespaceAndName())) { + log.warn("Duplicate repository found. Adding suffix DUPLICATE to repository {}", repository); + repository.setName(repository.getName() + "-" + repositoryId + "-DUPLICATE"); + } byNamespaceAndName.put(repository.getNamespaceAndName(), repository); byId.put(repositoryId, repository); } catch (InternalRepositoryException e) { diff --git a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java index 57d46f074e..657bf4b3d8 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java @@ -88,7 +88,7 @@ class XmlRepositoryDAOTest { @BeforeEach void createDAO(@TempDir Path basePath) { when(locationResolver.create(Path.class)).thenReturn( - new RepositoryLocationResolver.RepositoryLocationResolverInstance() { + new RepositoryLocationResolver.RepositoryLocationResolverInstance<>() { @Override public Path getLocation(String repositoryId) { return locationResolver.create(repositoryId); @@ -373,7 +373,7 @@ class XmlRepositoryDAOTest { private String content(Path storePath) { try { - return new String(Files.readAllBytes(storePath), Charsets.UTF_8); + return Files.readString(storePath, Charsets.UTF_8); } catch (IOException e) { throw new RuntimeException(e); } @@ -391,9 +391,7 @@ class XmlRepositoryDAOTest { void createMetadataFileForRepository(@TempDir Path basePath) throws IOException { repositoryPath = basePath.resolve("existing"); - Files.createDirectories(repositoryPath); - URL metadataUrl = Resources.getResource("sonia/scm/store/repositoryDaoMetadata.xml"); - Files.copy(metadataUrl.openStream(), repositoryPath.resolve("metadata.xml")); + prepareRepositoryPath(repositoryPath); } @Test @@ -448,6 +446,44 @@ class XmlRepositoryDAOTest { } } + @Nested + class WithDuplicateRepositories { + private Path repositoryPath; + private Path duplicateRepositoryPath; + + @BeforeEach + void createMetadataFileForRepository(@TempDir Path basePath) throws IOException { + repositoryPath = basePath.resolve("existing"); + duplicateRepositoryPath = basePath.resolve("duplicate"); + + prepareRepositoryPath(repositoryPath); + prepareRepositoryPath(duplicateRepositoryPath); + } + + @Test + void shouldRenameDuplicateRepositories() { + mockExistingPath(); + + XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem, repositoryExportingCheck); + + assertThat(dao.contains(new NamespaceAndName("space", "existing"))).isTrue(); + assertThat(dao.contains(new NamespaceAndName("space", "existing-existing2-DUPLICATE"))).isTrue(); + } + + private void mockExistingPath() { + triggeredOnForAllLocations = consumer -> { + consumer.accept("existing", repositoryPath); + consumer.accept("existing2", duplicateRepositoryPath); + }; + } + } + + private void prepareRepositoryPath(Path repositoryPath) throws IOException { + Files.createDirectories(repositoryPath); + URL metadataUrl = Resources.getResource("sonia/scm/store/repositoryDaoMetadata.xml"); + Files.copy(metadataUrl.openStream(), repositoryPath.resolve("metadata.xml")); + } + private Repository createRepository(String id) { return new Repository(id, "xml", "space", id); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryConfigInitializer.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryConfigInitializer.java index 3a6f2b31e5..6d986a5072 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryConfigInitializer.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryConfigInitializer.java @@ -29,11 +29,16 @@ import com.google.common.base.Strings; import sonia.scm.EagerSingleton; import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; import sonia.scm.plugin.Extension; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.store.ConfigurationStore; -import javax.inject.Inject; +import java.io.IOException; import java.util.Comparator; import java.util.List; +import java.util.Optional; + +import javax.inject.Inject; @Extension @EagerSingleton @@ -41,22 +46,31 @@ public class GitRepositoryConfigInitializer { private final GitRepositoryHandler repoHandler; private final GitRepositoryConfigStoreProvider storeProvider; + private final RepositoryServiceFactory serviceFactory; @Inject - public GitRepositoryConfigInitializer(GitRepositoryHandler repoHandler, GitRepositoryConfigStoreProvider storeProvider) { + public GitRepositoryConfigInitializer(GitRepositoryHandler repoHandler, GitRepositoryConfigStoreProvider storeProvider, RepositoryServiceFactory serviceFactory) { this.repoHandler = repoHandler; this.storeProvider = storeProvider; + this.serviceFactory = serviceFactory; } @Subscribe - public void initConfig(PostReceiveRepositoryHookEvent event) { + public void initConfig(PostReceiveRepositoryHookEvent event) throws IOException { if (GitRepositoryHandler.TYPE_NAME.equals(event.getRepository().getType())) { ConfigurationStore store = storeProvider.get(event.getRepository()); GitRepositoryConfig repositoryConfig = store.get(); if (repositoryConfig == null || Strings.isNullOrEmpty(repositoryConfig.getDefaultBranch())) { - List defaultBranchCandidates = event.getContext().getBranchProvider().getCreatedOrModified(); - - String defaultBranch = determineDefaultBranch(defaultBranchCandidates); + String defaultBranch; + try (RepositoryService service = serviceFactory.create(event.getRepository())) { + List branches = service.getBranchesCommand().getBranches().getBranches(); + Optional repoDefaultBranch = branches.stream().filter(Branch::isDefaultBranch).findFirst(); + if (repoDefaultBranch.isPresent()) { + defaultBranch = repoDefaultBranch.get().getName(); + } else { + defaultBranch = determineDefaultBranchFromPush(event); + } + } GitRepositoryConfig gitRepositoryConfig = new GitRepositoryConfig(defaultBranch); store.set(gitRepositoryConfig); @@ -64,7 +78,8 @@ public class GitRepositoryConfigInitializer { } } - private String determineDefaultBranch(List defaultBranchCandidates) { + private String determineDefaultBranchFromPush(PostReceiveRepositoryHookEvent event) { + List defaultBranchCandidates = event.getContext().getBranchProvider().getCreatedOrModified(); String globalConfigDefaultBranch = repoHandler.getConfig().getDefaultBranch(); if (defaultBranchCandidates.contains(globalConfigDefaultBranch)) { return globalConfigDefaultBranch; diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryConfigInitializerTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryConfigInitializerTest.java index 2278fc00fe..bfe4f5499e 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryConfigInitializerTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryConfigInitializerTest.java @@ -28,18 +28,24 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; +import sonia.scm.repository.api.BranchesCommandBuilder; import sonia.scm.repository.api.HookBranchProvider; import sonia.scm.repository.api.HookContext; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.store.ConfigurationStore; +import java.io.IOException; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -62,11 +68,20 @@ class GitRepositoryConfigInitializerTest { @Mock private PostReceiveRepositoryHookEvent event; + @Mock + private RepositoryServiceFactory serviceFactory; + @Mock + private RepositoryService service; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private BranchesCommandBuilder branchesCommand; + @Mock + private Branches branches; + @InjectMocks private GitRepositoryConfigInitializer initializer; @Test - void shouldSkipNonGitRepositories() { + void shouldSkipNonGitRepositories() throws IOException { REPOSITORY.setType("svn"); initializer.initConfig(event); @@ -83,13 +98,16 @@ class GitRepositoryConfigInitializerTest { class ForGitRepositories { @BeforeEach - void initMocks() { + void initMocks() throws IOException { when(storeProvider.get(REPOSITORY)).thenReturn(configStore); REPOSITORY.setType("git"); + lenient().when(serviceFactory.create(event.getRepository())).thenReturn(service); + lenient().when(service.getBranchesCommand()).thenReturn(branchesCommand); + lenient().when(branchesCommand.getBranches()).thenReturn(branches); } @Test - void shouldDoNothingIfDefaultBranchAlreadySet() { + void shouldDoNothingIfDefaultBranchAlreadySet() throws IOException { when(configStore.get()).thenReturn(new GitRepositoryConfig("any")); initializer.initConfig(event); @@ -104,11 +122,12 @@ class GitRepositoryConfigInitializerTest { void initRepoHandler() { GitConfig gitConfig = new GitConfig(); gitConfig.setDefaultBranch("global_default"); - when(repoHandler.getConfig()).thenReturn(gitConfig); + lenient().when(repoHandler.getConfig()).thenReturn(gitConfig); } + @Test - void shouldSetDefaultBranchIfNoGitConfigYet() { + void shouldSetDefaultBranchFromPushIfNoGitConfigYet() throws IOException { when(configStore.get()).thenReturn(null); initEvent(List.of("main")); @@ -118,7 +137,7 @@ class GitRepositoryConfigInitializerTest { } @Test - void shouldDetermineAndApplyDefaultBranch_GlobalDefault() { + void shouldDetermineAndApplyDefaultBranch_GlobalDefault() throws IOException { initEvent(List.of("global_default", "main", "master")); when(configStore.get()).thenReturn(new GitRepositoryConfig(null)); @@ -131,7 +150,7 @@ class GitRepositoryConfigInitializerTest { } @Test - void shouldDetermineAndApplyDefaultBranch_Main() { + void shouldDetermineAndApplyDefaultBranch_Main() throws IOException { initEvent(List.of("master", "main")); when(configStore.get()).thenReturn(new GitRepositoryConfig(null)); @@ -144,7 +163,7 @@ class GitRepositoryConfigInitializerTest { } @Test - void shouldDetermineAndApplyDefaultBranch_Master() { + void shouldDetermineAndApplyDefaultBranch_Master() throws IOException { initEvent(List.of("develop", "master")); when(configStore.get()).thenReturn(new GitRepositoryConfig(null)); @@ -157,7 +176,21 @@ class GitRepositoryConfigInitializerTest { } @Test - void shouldDetermineAndApplyDefaultBranch_BestMatch() { + void shouldDetermineAndApplyDefaultBranchFromRepository_Staging() throws IOException { + when(configStore.get()).thenReturn(null); + initEvent(List.of("master", "main")); + when(branches.getBranches()).thenReturn(List.of(new Branch("staging", "abc", true))); + + initializer.initConfig(event); + + verify(configStore).set(argThat(arg -> { + assertThat(arg.getDefaultBranch()).isEqualTo("staging"); + return true; + })); + } + + @Test + void shouldDetermineAndApplyDefaultBranch_BestMatch() throws IOException { initEvent(List.of("test/123", "trillian", "dent")); when(configStore.get()).thenReturn(new GitRepositoryConfig(null)); @@ -170,7 +203,7 @@ class GitRepositoryConfigInitializerTest { } @Test - void shouldDetermineAndApplyDefaultBranch_AnyFallback() { + void shouldDetermineAndApplyDefaultBranch_AnyFallback() throws IOException { initEvent(List.of("test/123", "x/y/z")); when(configStore.get()).thenReturn(new GitRepositoryConfig(null)); @@ -185,10 +218,10 @@ class GitRepositoryConfigInitializerTest { private void initEvent(List branches) { HookContext hookContext = mock(HookContext.class); - when(event.getContext()).thenReturn(hookContext); + lenient().when(event.getContext()).thenReturn(hookContext); HookBranchProvider branchProvider = mock(HookBranchProvider.class); - when(hookContext.getBranchProvider()).thenReturn(branchProvider); - when(branchProvider.getCreatedOrModified()).thenReturn(branches); + lenient().when(hookContext.getBranchProvider()).thenReturn(branchProvider); + lenient().when(branchProvider.getCreatedOrModified()).thenReturn(branches); } } } diff --git a/scm-webapp/src/main/java/sonia/scm/filter/SecurityHeadersFilter.java b/scm-webapp/src/main/java/sonia/scm/filter/SecurityHeadersFilter.java index 652aa78361..0777cdce48 100644 --- a/scm-webapp/src/main/java/sonia/scm/filter/SecurityHeadersFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/filter/SecurityHeadersFilter.java @@ -54,8 +54,8 @@ public class SecurityHeadersFilter extends HttpFilter { response.setHeader("X-Content-Type-Options", "nosniff"); response.setHeader("Content-Security-Policy", "form-action 'self'; " + - "object-src 'none'; " + - "frame-ancestors 'none'; " + + "object-src 'self'; " + + "frame-ancestors 'self'; " + "block-all-mixed-content" ); response.setHeader("Permissions-Policy", 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 65c6da4115..253ee43f91 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java @@ -30,6 +30,7 @@ import com.google.inject.Inject; import com.google.inject.Singleton; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.AlreadyExistsException; import sonia.scm.ConfigurationException; import sonia.scm.HandlerEventType; import sonia.scm.ManagerDaoAdapter; @@ -183,7 +184,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { }, newRepository -> { if (repositoryDAO.contains(newRepository.getNamespaceAndName())) { - throw alreadyExists(entity(newRepository.getClass(), newRepository.getNamespaceAndName().logString())); + throw alreadyExists(entity(newRepository.getNamespaceAndName())); } } ); @@ -292,10 +293,16 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { public Repository rename(Repository repository, String newNamespace, String newName) { + NamespaceAndName newNamespaceAndName = new NamespaceAndName(newNamespace, newName); + if (hasNamespaceOrNameNotChanged(repository, newNamespace, newName)) { throw new NoChangesMadeException(repository); } + if (this.get(newNamespaceAndName) != null){ + throw AlreadyExistsException.alreadyExists(entity(NamespaceAndName.class, newNamespaceAndName.logString())); + } + Repository changedRepository = repository.clone(); if (!Strings.isNullOrEmpty(newName)) { changedRepository.setName(newName); diff --git a/scm-webapp/src/main/java/sonia/scm/security/ApiKeyTokenHandler.java b/scm-webapp/src/main/java/sonia/scm/security/ApiKeyTokenHandler.java index de38346aec..83d3e6db24 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/ApiKeyTokenHandler.java +++ b/scm-webapp/src/main/java/sonia/scm/security/ApiKeyTokenHandler.java @@ -64,9 +64,7 @@ class ApiKeyTokenHandler { return of(OBJECT_MAPPER.readValue(decoder.decode(token), Token.class)); } catch (IOException | DecodingException e) { LOG.debug("failed to read api token, perhaps it is a jwt token or a normal password"); - if (LOG.isTraceEnabled()) { - LOG.trace("failed to parse token", e); - } + // do not print the exception here, because it could reveal password details return empty(); } } 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 dff97c98cd..1c479640c7 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java @@ -457,6 +457,16 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase { assertEquals("hitchhiker", changedRepo.getNamespace()); } + @Test + public void shouldNotRenameRepositoryIfNameOrNamespaceAlreadyInUse() { + Repository repository1 = createTestRepository(); + Repository repository2 = createSecondTestRepository(); + RepositoryManager repoManager = (RepositoryManager) manager; + + assertThrows(AlreadyExistsException.class, () -> repoManager.rename(repository2, repository1.getNamespace(), repository1.getName())); + + } + @Test public void shouldReturnDistinctNamespaces() { createTestRepository();