diff --git a/CHANGELOG.md b/CHANGELOG.md index acd792468c..776b58e72a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Internal server error for git sub modules without tree object ([#1397](https://github.com/scm-manager/scm-manager/pull/1397)) - Do not expose subversion commit with id 0 ([#1395](https://github.com/scm-manager/scm-manager/pull/1395)) +- Cloning of Mercurial repositories with api keys ([#1407](https://github.com/scm-manager/scm-manager/pull/1407)) - Disable cloning repositories via ssh for anonymous users ([#1403](https://github.com/scm-manager/scm-manager/pull/1403)) - Support anonymous file download through rest api for non-browser clients (e.g. curl or postman) when anonymous mode is set to protocol-only ([#1402](https://github.com/scm-manager/scm-manager/pull/1402)) - SVN diff with property changes ([#1400](https://github.com/scm-manager/scm-manager/pull/1400)) diff --git a/scm-it/src/test/java/sonia/scm/it/ApiKeyITCase.java b/scm-it/src/test/java/sonia/scm/it/ApiKeyITCase.java index b9f75a9dec..24ce46581f 100644 --- a/scm-it/src/test/java/sonia/scm/it/ApiKeyITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/ApiKeyITCase.java @@ -30,6 +30,8 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; import sonia.scm.it.utils.RepositoryUtil; import sonia.scm.it.utils.RestUtil; import sonia.scm.it.utils.TestData; @@ -39,16 +41,30 @@ import sonia.scm.web.VndMediaType; import javax.ws.rs.core.MediaType; import java.io.IOException; +import java.util.Collection; import java.util.Objects; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; import static sonia.scm.it.utils.RepositoryUtil.addAndCommitRandomFile; import static sonia.scm.it.utils.RestUtil.given; +import static sonia.scm.it.utils.ScmTypes.availableScmTypes; import static sonia.scm.it.utils.TestData.WRITE; +@RunWith(Parameterized.class) public class ApiKeyITCase { + @Parameterized.Parameters(name = "{0}") + public static Collection createParameters() { + return availableScmTypes(); + } + + private final String repositoryType; + + public ApiKeyITCase(String repositoryType) { + this.repositoryType = repositoryType; + } + @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @@ -56,7 +72,7 @@ public class ApiKeyITCase { public void prepareEnvironment() { TestData.createDefault(); TestData.createNotAdminUser("user", "user"); - TestData.createUserPermission("user", WRITE, "git"); + TestData.createUserPermission("user", WRITE, repositoryType); } @After @@ -68,7 +84,7 @@ public class ApiKeyITCase { public void shouldCloneWithRestrictedApiKey() throws IOException { String passphrase = registerApiKey(); - RepositoryClient client = RepositoryUtil.createRepositoryClient("git", temporaryFolder.newFolder(), "user", passphrase); + RepositoryClient client = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.newFolder(), "user", passphrase); assertEquals(1, Objects.requireNonNull(client.getWorkingCopy().list()).length); } @@ -77,7 +93,7 @@ public class ApiKeyITCase { public void shouldFailToCommit() throws IOException { String passphrase = registerApiKey(); - RepositoryClient client = RepositoryUtil.createRepositoryClient("git", temporaryFolder.newFolder(), "user", passphrase); + RepositoryClient client = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.newFolder(), "user", passphrase); assertThrows(RepositoryClientException.class, () -> addAndCommitRandomFile(client, "user")); } diff --git a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java index 5221142fc0..2455a01eba 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java +++ b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java @@ -40,9 +40,7 @@ import java.time.Clock; import java.time.Instant; import java.util.Date; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; -import java.util.Set; import java.util.concurrent.TimeUnit; /** @@ -71,7 +69,6 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder { private Instant refreshExpiration; private String parentKeyId; private Scope scope = Scope.empty(); - private Set groups = new HashSet<>(); private final Map custom = Maps.newHashMap(); @@ -155,8 +152,13 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder { @Override public JwtAccessToken build() { - if (SecurityUtils.getSubject().getPrincipals().getRealmNames().contains(ApiKeyRealm.NAME)) { - throw new AuthorizationException("Cannot create access token for api keys"); + final Scope principalScope = SecurityUtils.getSubject().getPrincipals().oneByType(Scope.class); + if (principalScope != null && !principalScope.isEmpty()) { + if (scope != null && !scope.isEmpty()) { + throw new AuthorizationException(String.format("cannot merge builder scope (%s) with principal scope (%s)", scope, principalScope)); + } + LOG.debug("using existing scope for new access token: {}", principalScope); + scope = principalScope; } String id = keyGenerator.createKey(); diff --git a/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenBuilderTest.java b/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenBuilderTest.java index 40234781d4..3f36a4e341 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenBuilderTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenBuilderTest.java @@ -39,6 +39,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.util.Collection; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -135,14 +136,27 @@ class JwtAccessTokenBuilderTest { @Nested class FromApiKeyRealm { + private Scope scope; + @BeforeEach void mockApiKeyRealm() { + scope = Scope.valueOf("dummy:scope:*"); lenient().when(principalCollection.getRealmNames()).thenReturn(singleton("ApiTokenRealm")); + lenient().when(principalCollection.oneByType(Scope.class)).thenReturn(scope); } @Test - void testRejectedRequest() { + void shouldCreateJwtAndUsePreviousScope() { JwtAccessTokenBuilder builder = factory.create().subject("dent"); + final JwtAccessToken accessToken = builder.build(); + assertThat(accessToken).isNotNull(); + assertThat(accessToken.getSubject()).isEqualTo("dent"); + assertThat((Collection) accessToken.getCustom("scope").get()).containsExactly("dummy:scope:*"); + } + + @Test + void shouldThrowExceptionWhenScopeAlreadyDefinedInBuilder() { + JwtAccessTokenBuilder builder = factory.create().scope(Scope.valueOf("an:incompatible:scope")).subject("dent"); assertThrows(AuthorizationException.class, builder::build); } }