From 6217a757e338d090bf5cd62b47371764a4bd78af Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 13 Feb 2020 09:16:29 +0100 Subject: [PATCH 01/46] Add xsrf token to hg callbacks --- scm-plugins/scm-hg-plugin/pom.xml | 13 ++++++ .../sonia/scm/repository/HgEnvironment.java | 20 +++++++- .../sonia/scm/repository/HgHookManager.java | 4 +- .../resources/sonia/scm/python/scmhooks.py | 4 +- .../scm/repository/HgEnvironmentTest.java | 46 +++++++++++++++++++ 5 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgEnvironmentTest.java diff --git a/scm-plugins/scm-hg-plugin/pom.xml b/scm-plugins/scm-hg-plugin/pom.xml index e57652bf0f..2dfd89c8fb 100644 --- a/scm-plugins/scm-hg-plugin/pom.xml +++ b/scm-plugins/scm-hg-plugin/pom.xml @@ -27,6 +27,19 @@ + + + io.jsonwebtoken + jjwt-impl + 0.10.5 + provided + + + io.jsonwebtoken + jjwt-jackson + 0.10.5 + test + diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java index 1d227fb54e..085e1516eb 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java @@ -36,8 +36,11 @@ package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- import com.google.inject.ProvisionException; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.security.CipherUtil; import sonia.scm.web.HgUtil; import javax.servlet.http.HttpServletRequest; @@ -65,6 +68,8 @@ public final class HgEnvironment private static final String SCM_BEARER_TOKEN = "SCM_BEARER_TOKEN"; + private static final String SCM_XSRF = "SCM_XSRF"; + //~--- constructors --------------------------------------------------------- /** @@ -115,7 +120,8 @@ public final class HgEnvironment try { String credentials = hookManager.getCredentials(); - environment.put(SCM_BEARER_TOKEN, credentials); + environment.put(SCM_BEARER_TOKEN, CipherUtil.getInstance().encode(credentials)); + extractXsrfKey(environment, credentials); } catch (ProvisionException e) { LOG.debug("could not create bearer token; looks like currently we are not in a request; probably you can ignore the following exception:", e); } @@ -123,4 +129,16 @@ public final class HgEnvironment environment.put(ENV_URL, hookUrl); environment.put(ENV_CHALLENGE, hookManager.getChallenge()); } + + private static void extractXsrfKey(Map environment, String credentials) { + // we need to remove the signature, because we cannot access the key and otherwise the parser would fail + String[] tokenParts = credentials.split("\\."); + String tokenWithoutSignature = tokenParts[0] + "." + tokenParts[1] + "."; + Claims claims = (Claims) Jwts.parser().parse(tokenWithoutSignature).getBody(); + + Object xsrf = claims.get("xsrf"); + if (xsrf != null) { + environment.put(SCM_XSRF, xsrf.toString()); + } + } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgHookManager.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgHookManager.java index 6815bdad96..9784f3d49d 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgHookManager.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgHookManager.java @@ -200,7 +200,7 @@ public class HgHookManager { AccessToken accessToken = accessTokenBuilderFactory.create().build(); - return CipherUtil.getInstance().encode(accessToken.compact()); + return accessToken.compact(); } //~--- methods -------------------------------------------------------------- @@ -279,7 +279,7 @@ public class HgHookManager //J- return HttpUtil.getUriWithoutEndSeperator( MoreObjects.firstNonNull( - configuration.getBaseUrl(), + configuration.getBaseUrl(), "http://localhost:8080/scm" ) ).concat("/hook/hg/"); diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/scmhooks.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/scmhooks.py index 637aa16331..ca8d7736a7 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/scmhooks.py +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/scmhooks.py @@ -41,6 +41,7 @@ import os, urllib, urllib2 baseUrl = os.environ['SCM_URL'] challenge = os.environ['SCM_CHALLENGE'] token = os.environ['SCM_BEARER_TOKEN'] +xsrf = os.environ['SCM_XSRF'] repositoryId = os.environ['SCM_REPOSITORY_ID'] def printMessages(ui, msgs): @@ -59,6 +60,7 @@ def callHookUrl(ui, repo, hooktype, node): proxy_handler = urllib2.ProxyHandler({}) opener = urllib2.build_opener(proxy_handler) req = urllib2.Request(url, data) + req.add_header("X-XSRF-Token", xsrf) conn = opener.open(req) if 200 <= conn.code < 300: ui.debug( "scm-hook " + hooktype + " success with status code " + str(conn.code) + "\n" ) @@ -101,7 +103,7 @@ def preHook(ui, repo, hooktype, node=None, source=None, pending=None, **kwargs): # older mercurial versions if pending != None: pending() - + # newer mercurial version # we have to make in-memory changes visible to external process # this does not happen automatically, because mercurial treat our hooks as internal hooks diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgEnvironmentTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgEnvironmentTest.java new file mode 100644 index 0000000000..2d4468122a --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgEnvironmentTest.java @@ -0,0 +1,46 @@ +package sonia.scm.repository; + + +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.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class HgEnvironmentTest { + + private static final String CREDENTIALS_WITH_XSRF = "eyJhbGciOiJIUzI1NiJ9.eyJ4c3JmIjoiZjlhMWRiNzQtM2UwNS00YTMwLTlkODMtNjZmNWQ1MDc3Y2FjIiwic3ViIjoic2NtYWRtaW4iLCJqdGkiOiI2d1JxTWpyelYxSCIsImlhdCI6MTU4MTU4MTI3OSwiZXhwIjoxNTgxNTg0ODc5LCJzY20tbWFuYWdlci5yZWZyZXNoRXhwaXJhdGlvbiI6MTU4MTYyNDQ3OTczMCwic2NtLW1hbmFnZXIucGFyZW50VG9rZW5JZCI6IjZ3UnFNanJ6VjFIIn0.O5MADk9scaHgYNPDFh7Nd9R2rMZyDuMs7LuC4OSA3jA"; + private static final String CREDENTIALS_WITHOUT_XSRF = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzY21hZG1pbiIsImp0aSI6IjdvUnFNbERrRTFOIiwiaWF0IjoxNTgxNTgxNjAxLCJleHAiOjE1ODE1ODUyMDEsInNjbS1tYW5hZ2VyLnJlZnJlc2hFeHBpcmF0aW9uIjoxNTgxNjI0ODAxNjc5LCJzY20tbWFuYWdlci5wYXJlbnRUb2tlbklkIjoiN29ScU1sRGtFMU4ifQ.KaTPjT09xtIEZDBOM28pSgyYSEtVZ37gcyTp1_3sTGA"; + + @Mock + HgRepositoryHandler handler; + @Mock + HgHookManager hookManager; + + @Test + void shouldExtractXsrfTokenWhenSet() { + when(hookManager.getCredentials()).thenReturn(CREDENTIALS_WITH_XSRF); + + Map environment = new HashMap<>(); + HgEnvironment.prepareEnvironment(environment, handler, hookManager); + + assertThat(environment).contains(entry("SCM_XSRF", "f9a1db74-3e05-4a30-9d83-66f5d5077cac")); + } + + @Test + void shouldIgnoreXsrfWhenNotSet() { + when(hookManager.getCredentials()).thenReturn(CREDENTIALS_WITHOUT_XSRF); + + Map environment = new HashMap<>(); + HgEnvironment.prepareEnvironment(environment, handler, hookManager); + + assertThat(environment).doesNotContainKeys("SCM_XSRF"); + } +} From 1ebad2f0802a440271fbb85997b1e78eb7ccb9f7 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 13 Feb 2020 09:20:26 +0100 Subject: [PATCH 02/46] Log change --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4f080c9e0..ebfa5b64f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Upgrade [Mockito](https://site.mockito.org/) to version 2.28.2 in order to fix tests on Java versions > 8 - Upgrade smp-maven-plugin to version 1.0.0-rc3 +### Fixed +- Modification for mercurial repositories with enabled XSRF protection + ## 2.0.0-rc3 - 2020-01-31 ### Fixed - Broken plugin order fixed From c4dee747e32303f41961b2f8f70e636e503ca318 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 13 Feb 2020 10:34:40 +0100 Subject: [PATCH 03/46] Put default for xsrf environment key Otherwise the python script scmhooks.py fails, because the environment access with a missing key raises an error. --- .../src/main/java/sonia/scm/repository/HgEnvironment.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java index 085e1516eb..1bcbcc321a 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java @@ -139,6 +139,8 @@ public final class HgEnvironment Object xsrf = claims.get("xsrf"); if (xsrf != null) { environment.put(SCM_XSRF, xsrf.toString()); + } else { + environment.put(SCM_XSRF, "-"); } } } From defad2af5bbc54f283e363a0ca7f1a625c931fc0 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 13 Feb 2020 11:27:25 +0100 Subject: [PATCH 04/46] Remove token expiration from test --- .../java/sonia/scm/repository/HgEnvironmentTest.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgEnvironmentTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgEnvironmentTest.java index 2d4468122a..678a1f70c5 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgEnvironmentTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgEnvironmentTest.java @@ -16,8 +16,8 @@ import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class HgEnvironmentTest { - private static final String CREDENTIALS_WITH_XSRF = "eyJhbGciOiJIUzI1NiJ9.eyJ4c3JmIjoiZjlhMWRiNzQtM2UwNS00YTMwLTlkODMtNjZmNWQ1MDc3Y2FjIiwic3ViIjoic2NtYWRtaW4iLCJqdGkiOiI2d1JxTWpyelYxSCIsImlhdCI6MTU4MTU4MTI3OSwiZXhwIjoxNTgxNTg0ODc5LCJzY20tbWFuYWdlci5yZWZyZXNoRXhwaXJhdGlvbiI6MTU4MTYyNDQ3OTczMCwic2NtLW1hbmFnZXIucGFyZW50VG9rZW5JZCI6IjZ3UnFNanJ6VjFIIn0.O5MADk9scaHgYNPDFh7Nd9R2rMZyDuMs7LuC4OSA3jA"; - private static final String CREDENTIALS_WITHOUT_XSRF = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzY21hZG1pbiIsImp0aSI6IjdvUnFNbERrRTFOIiwiaWF0IjoxNTgxNTgxNjAxLCJleHAiOjE1ODE1ODUyMDEsInNjbS1tYW5hZ2VyLnJlZnJlc2hFeHBpcmF0aW9uIjoxNTgxNjI0ODAxNjc5LCJzY20tbWFuYWdlci5wYXJlbnRUb2tlbklkIjoiN29ScU1sRGtFMU4ifQ.KaTPjT09xtIEZDBOM28pSgyYSEtVZ37gcyTp1_3sTGA"; + private static final String CREDENTIALS_WITH_XSRF = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkZW50Iiwic2NtLW1hbmFnZXIucGFyZW50VG9rZW5JZCI6IkFCQyIsInhzcmYiOiJYU1JGIFRva2VuIiwiaWF0IjoxNTgxNTg3MzUzLCJqdGkiOiJFV1JxTjlNMTQ5In0.jgsIoE_2TnTEwbuaqQp8XyKpId5qlYURmYamf9m_08w"; + private static final String CREDENTIALS_WITHOUT_XSRF = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkZW50Iiwic2NtLW1hbmFnZXIucGFyZW50VG9rZW5JZCI6IkFCQyIsImlhdCI6MTU4MTU4NzM1MywianRpIjoiRVdScU45TTE0OSJ9.VdMz5-NpREiIvLEw9JVJNEUnoY0am0j1lZ0kisblayk"; @Mock HgRepositoryHandler handler; @@ -31,16 +31,16 @@ class HgEnvironmentTest { Map environment = new HashMap<>(); HgEnvironment.prepareEnvironment(environment, handler, hookManager); - assertThat(environment).contains(entry("SCM_XSRF", "f9a1db74-3e05-4a30-9d83-66f5d5077cac")); + assertThat(environment).contains(entry("SCM_XSRF", "XSRF Token")); } @Test - void shouldIgnoreXsrfWhenNotSet() { + void shouldIgnoreXsrfWhenNotSetButStillContainDummy() { when(hookManager.getCredentials()).thenReturn(CREDENTIALS_WITHOUT_XSRF); Map environment = new HashMap<>(); HgEnvironment.prepareEnvironment(environment, handler, hookManager); - assertThat(environment).doesNotContainKeys("SCM_XSRF"); + assertThat(environment).containsKeys("SCM_XSRF"); } } From 97cc0e7b9c96a96ebe648c50e9b60ab49f04aa13 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Mon, 17 Feb 2020 11:08:08 +0100 Subject: [PATCH 05/46] Use access key directly, not the jwt token --- .../main/java/sonia/scm/security/Xsrf.java | 10 ++++---- .../sonia/scm/repository/HgEnvironment.java | 24 ++++++------------- .../sonia/scm/repository/HgHookManager.java | 7 ++---- .../scm/repository/HgEnvironmentTest.java | 18 ++++++++++---- .../java/sonia/scm/repository/HgTestUtil.java | 6 +++-- 5 files changed, 31 insertions(+), 34 deletions(-) rename {scm-webapp => scm-core}/src/main/java/sonia/scm/security/Xsrf.java (94%) diff --git a/scm-webapp/src/main/java/sonia/scm/security/Xsrf.java b/scm-core/src/main/java/sonia/scm/security/Xsrf.java similarity index 94% rename from scm-webapp/src/main/java/sonia/scm/security/Xsrf.java rename to scm-core/src/main/java/sonia/scm/security/Xsrf.java index f9ee8a0872..83e83c80b4 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/Xsrf.java +++ b/scm-core/src/main/java/sonia/scm/security/Xsrf.java @@ -32,15 +32,15 @@ package sonia.scm.security; /** * Shared constants for Xsrf related classes. - * + * * @author Sebastian Sdorra * @since 2.0.0 */ public final class Xsrf { - - static final String HEADER_KEY = "X-XSRF-Token"; - - static final String TOKEN_KEY = "xsrf"; + + public static final String HEADER_KEY = "X-XSRF-Token"; + + public static final String TOKEN_KEY = "xsrf"; private Xsrf() { } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java index 1bcbcc321a..a9328c1129 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java @@ -36,11 +36,11 @@ package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- import com.google.inject.ProvisionException; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.security.AccessToken; import sonia.scm.security.CipherUtil; +import sonia.scm.security.Xsrf; import sonia.scm.web.HgUtil; import javax.servlet.http.HttpServletRequest; @@ -119,9 +119,9 @@ public final class HgEnvironment } try { - String credentials = hookManager.getCredentials(); - environment.put(SCM_BEARER_TOKEN, CipherUtil.getInstance().encode(credentials)); - extractXsrfKey(environment, credentials); + AccessToken accessToken = hookManager.getAccessToken(); + environment.put(SCM_BEARER_TOKEN, CipherUtil.getInstance().encode(accessToken.compact())); + extractXsrfKey(environment, accessToken); } catch (ProvisionException e) { LOG.debug("could not create bearer token; looks like currently we are not in a request; probably you can ignore the following exception:", e); } @@ -130,17 +130,7 @@ public final class HgEnvironment environment.put(ENV_CHALLENGE, hookManager.getChallenge()); } - private static void extractXsrfKey(Map environment, String credentials) { - // we need to remove the signature, because we cannot access the key and otherwise the parser would fail - String[] tokenParts = credentials.split("\\."); - String tokenWithoutSignature = tokenParts[0] + "." + tokenParts[1] + "."; - Claims claims = (Claims) Jwts.parser().parse(tokenWithoutSignature).getBody(); - - Object xsrf = claims.get("xsrf"); - if (xsrf != null) { - environment.put(SCM_XSRF, xsrf.toString()); - } else { - environment.put(SCM_XSRF, "-"); - } + private static void extractXsrfKey(Map environment, AccessToken accessToken) { + environment.put(SCM_XSRF, accessToken.getCustom(Xsrf.TOKEN_KEY).orElse("-")); } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgHookManager.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgHookManager.java index 9784f3d49d..314bd85b57 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgHookManager.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgHookManager.java @@ -49,7 +49,6 @@ import sonia.scm.config.ScmConfigurationChangedEvent; import sonia.scm.net.ahc.AdvancedHttpClient; import sonia.scm.security.AccessToken; import sonia.scm.security.AccessTokenBuilderFactory; -import sonia.scm.security.CipherUtil; import sonia.scm.util.HttpUtil; import sonia.scm.util.Util; @@ -196,11 +195,9 @@ public class HgHookManager return this.challenge.equals(challenge); } - public String getCredentials() + public AccessToken getAccessToken() { - AccessToken accessToken = accessTokenBuilderFactory.create().build(); - - return accessToken.compact(); + return accessTokenBuilderFactory.create().build(); } //~--- methods -------------------------------------------------------------- diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgEnvironmentTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgEnvironmentTest.java index 678a1f70c5..2718e0b899 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgEnvironmentTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgEnvironmentTest.java @@ -5,20 +5,22 @@ 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.security.AccessToken; +import sonia.scm.security.Xsrf; import java.util.HashMap; import java.util.Map; +import static java.util.Optional.empty; +import static java.util.Optional.of; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class HgEnvironmentTest { - private static final String CREDENTIALS_WITH_XSRF = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkZW50Iiwic2NtLW1hbmFnZXIucGFyZW50VG9rZW5JZCI6IkFCQyIsInhzcmYiOiJYU1JGIFRva2VuIiwiaWF0IjoxNTgxNTg3MzUzLCJqdGkiOiJFV1JxTjlNMTQ5In0.jgsIoE_2TnTEwbuaqQp8XyKpId5qlYURmYamf9m_08w"; - private static final String CREDENTIALS_WITHOUT_XSRF = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkZW50Iiwic2NtLW1hbmFnZXIucGFyZW50VG9rZW5JZCI6IkFCQyIsImlhdCI6MTU4MTU4NzM1MywianRpIjoiRVdScU45TTE0OSJ9.VdMz5-NpREiIvLEw9JVJNEUnoY0am0j1lZ0kisblayk"; - @Mock HgRepositoryHandler handler; @Mock @@ -26,7 +28,10 @@ class HgEnvironmentTest { @Test void shouldExtractXsrfTokenWhenSet() { - when(hookManager.getCredentials()).thenReturn(CREDENTIALS_WITH_XSRF); + AccessToken accessToken = mock(AccessToken.class); + when(accessToken.compact()).thenReturn(""); + when(accessToken.getCustom(Xsrf.TOKEN_KEY)).thenReturn(of("XSRF Token")); + when(hookManager.getAccessToken()).thenReturn(accessToken); Map environment = new HashMap<>(); HgEnvironment.prepareEnvironment(environment, handler, hookManager); @@ -36,7 +41,10 @@ class HgEnvironmentTest { @Test void shouldIgnoreXsrfWhenNotSetButStillContainDummy() { - when(hookManager.getCredentials()).thenReturn(CREDENTIALS_WITHOUT_XSRF); + AccessToken accessToken = mock(AccessToken.class); + when(accessToken.compact()).thenReturn(""); + when(accessToken.getCustom(Xsrf.TOKEN_KEY)).thenReturn(empty()); + when(hookManager.getAccessToken()).thenReturn(accessToken); Map environment = new HashMap<>(); HgEnvironment.prepareEnvironment(environment, handler, hookManager); diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java index ee5117b276..a5be01465f 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java @@ -38,6 +38,7 @@ package sonia.scm.repository; import org.junit.Assume; import sonia.scm.SCMContext; import sonia.scm.TempDirRepositoryLocationResolver; +import sonia.scm.security.AccessToken; import sonia.scm.store.InMemoryConfigurationStoreFactory; import javax.servlet.http.HttpServletRequest; @@ -107,7 +108,6 @@ public final class HgTestUtil RepositoryLocationResolver repositoryLocationResolver = new TempDirRepositoryLocationResolver(directory); HgRepositoryHandler handler = new HgRepositoryHandler(new InMemoryConfigurationStoreFactory(), new HgContextProvider(), repositoryLocationResolver, null, null); - Path repoDir = directory.toPath(); handler.init(context); return handler; @@ -128,7 +128,9 @@ public final class HgTestUtil "http://localhost:8081/scm/hook/hg/"); when(hookManager.createUrl(any(HttpServletRequest.class))).thenReturn( "http://localhost:8081/scm/hook/hg/"); - when(hookManager.getCredentials()).thenReturn(""); + AccessToken accessToken = mock(AccessToken.class); + when(accessToken.compact()).thenReturn(""); + when(hookManager.getAccessToken()).thenReturn(accessToken); return hookManager; } From 7243f3d5a5507d954a409338a3e031475ebd2b09 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Mon, 17 Feb 2020 13:42:48 +0100 Subject: [PATCH 06/46] bootstrap openapi documentation --- scm-webapp/pom.xml | 59 ++++++++++++++++++- scm-webapp/src/main/doc/openapi.md | 3 + .../v2/resources/AuthenticationResource.java | 20 +++++++ .../api/v2/resources/RepositoryResource.java | 47 ++++++++++++--- .../v2/resources/RepositoryRootResource.java | 10 +++- 5 files changed, 128 insertions(+), 11 deletions(-) create mode 100644 scm-webapp/src/main/doc/openapi.md diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index 924e176024..4520700dbc 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -426,6 +426,13 @@ provided + + io.swagger.core.v3 + swagger-annotations + 2.1.1 + provided + + @@ -471,6 +478,50 @@ + + io.openapitools.swagger + swagger-maven-plugin + 2.1.2 + + + sonia.scm.api.v2.resources + + ${basedir}/target/openapi/META-INF/scm + openapi + JSON,YAML + true + + + + http://localhost:8081/scm/api + local endpoint url + + + + SCM-Manager REST-API + ${project.version} + + scmmanager@googlegroups.com + SCM-Manager + https://scm-manager.org + + + http://www.opensource.org/licenses/bsd-license.php + BSD + + + src/main/doc/openapi.md + + + + + + generate + + + + + sonia.scm.maven smp-maven-plugin @@ -511,9 +562,15 @@ org.apache.maven.plugins maven-war-plugin - 2.2 + 3.1.0 true + + + target/openapi + WEB-INF/classes + + diff --git a/scm-webapp/src/main/doc/openapi.md b/scm-webapp/src/main/doc/openapi.md new file mode 100644 index 0000000000..c014899c30 --- /dev/null +++ b/scm-webapp/src/main/doc/openapi.md @@ -0,0 +1,3 @@ +# openapi docs from code + +describe hateoas diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java index deab53a708..3bb37b99d4 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java @@ -3,6 +3,10 @@ package sonia.scm.api.v2.resources; import com.google.inject.Inject; import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.security.SecurityScheme; +import io.swagger.v3.oas.annotations.security.SecuritySchemes; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.subject.Subject; @@ -19,6 +23,22 @@ import javax.ws.rs.core.Response; import java.net.URI; import java.util.Optional; +@SecuritySchemes({ + @SecurityScheme( + name = "Basic Authentication", + description = "HTTP Basic authentication with username and password", + scheme = "Basic", + type = SecuritySchemeType.HTTP + ), + @SecurityScheme( + name = "Bearer Token Authentication", + in = SecuritySchemeIn.HEADER, + paramName = "Authorization", + scheme = "Bearer", + bearerFormat = "JWT", + type = SecuritySchemeType.APIKEY + ) +}) @Path(AuthenticationResource.PATH) @AllowAnonymousAccess public class AuthenticationResource { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index 2294dc600e..ebae9e53e9 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -3,6 +3,10 @@ package sonia.scm.api.v2.resources; import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryManager; @@ -87,14 +91,39 @@ public class RepositoryResource { @GET @Path("") @Produces(VndMediaType.REPOSITORY) - @TypeHint(RepositoryDto.class) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the repository"), - @ResponseCode(code = 404, condition = "not found, no repository with the specified name available in the namespace"), - @ResponseCode(code = 500, condition = "internal server error") - }) + @Operation(summary = "Returns a single repository", description = "Returns the repository for the given namespace and name.", tags = "Repository") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = VndMediaType.REPOSITORY, + schema = @Schema(implementation = RepositoryDto.class) + ) + ) + @ApiResponse( + responseCode = "401", + description = "not authenticated / invalid credentials" + ) + @ApiResponse( + responseCode = "403", + description = "not authorized, the current user has no privileges to read the repository" + ) + @ApiResponse( + responseCode = "404", + description = "not found, no repository with the specified name available in the namespace", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + ) + ) + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + ) + ) public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name){ return adapter.get(loadBy(namespace, name), repositoryToDtoMapper::map); } @@ -168,7 +197,7 @@ public class RepositoryResource { } @Path("branches/") - public BranchRootResource branches(@PathParam("namespace") String namespace, @PathParam("name") String name) { + public BranchRootResource branches() { return branchRootResource.get(); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRootResource.java index a7a6365c37..7b8c1120e1 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRootResource.java @@ -1,12 +1,20 @@ package sonia.scm.api.v2.resources; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.tags.Tag; + import javax.inject.Inject; import javax.inject.Provider; import javax.ws.rs.Path; /** - * RESTful Web Service Resource to manage repositories. + * RESTful Web Service Resource to manage repositories. */ +@OpenAPIDefinition( + tags = { + @Tag(name = "Repository", description = "Repository related endpoints") + } +) @Path(RepositoryRootResource.REPOSITORIES_PATH_V2) public class RepositoryRootResource { static final String REPOSITORIES_PATH_V2 = "v2/repositories/"; From b7e95f3cc4fe1d95d6ff4e14e98e3d1201e5171a Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Mon, 17 Feb 2020 14:09:26 +0100 Subject: [PATCH 07/46] create openapi docs for scm-git-plugin --- pom.xml | 12 ++++ .../sonia/scm/api/v2/resources/ErrorDto.java | 0 scm-plugins/pom.xml | 38 +++++++++++ .../api/v2/resources/GitConfigResource.java | 53 ++++++++++++--- .../GitRepositoryConfigResource.java | 65 +++++++++++++++---- scm-webapp/pom.xml | 1 - 6 files changed, 144 insertions(+), 25 deletions(-) rename {scm-webapp => scm-core}/src/main/java/sonia/scm/api/v2/resources/ErrorDto.java (100%) diff --git a/pom.xml b/pom.xml index 48e94c11de..535b9d9af8 100644 --- a/pom.xml +++ b/pom.xml @@ -266,6 +266,12 @@ ${jaxrs.version} + + io.swagger.core.v3 + swagger-annotations + 2.1.1 + + com.fasterxml.jackson.core jackson-core @@ -465,6 +471,12 @@ 2.8.2 + + io.openapitools.swagger + swagger-maven-plugin + 2.1.2 + + diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ErrorDto.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/ErrorDto.java similarity index 100% rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/ErrorDto.java rename to scm-core/src/main/java/sonia/scm/api/v2/resources/ErrorDto.java diff --git a/scm-plugins/pom.xml b/scm-plugins/pom.xml index e6b2929bd2..b74effd8ce 100644 --- a/scm-plugins/pom.xml +++ b/scm-plugins/pom.xml @@ -61,6 +61,13 @@ provided + + + io.swagger.core.v3 + swagger-annotations + provided + + @@ -136,6 +143,37 @@ + + io.openapitools.swagger + swagger-maven-plugin + + + sonia.scm.api.v2.resources + + ${basedir}/target/classes/META-INF/scm + openapi + JSON,YAML + true + + + SCM-Manager Plugin REST-API + ${project.version} + + http://www.opensource.org/licenses/bsd-license.php + BSD + + + + + + + + generate + + + + + diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java index 7cda4bc9d3..d3332d5a37 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java @@ -3,10 +3,17 @@ package sonia.scm.api.v2.resources; import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import sonia.scm.config.ConfigurationPermissions; import sonia.scm.repository.GitConfig; import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.web.GitVndMediaType; +import sonia.scm.web.VndMediaType; import javax.inject.Inject; import javax.inject.Provider; @@ -14,13 +21,15 @@ import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.PUT; import javax.ws.rs.Path; -import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.Response; /** * RESTful Web Service Resource to manage the configuration of the git plugin. */ +@OpenAPIDefinition(tags = { + @Tag(name = "Git", description = "Configuration for the git repository type") +}) @Path(GitConfigResource.GIT_CONFIG_PATH_V2) public class GitConfigResource { @@ -45,13 +54,24 @@ public class GitConfigResource { @GET @Path("") @Produces(GitVndMediaType.GIT_CONFIG) - @TypeHint(GitConfigDto.class) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:read:git\" privilege"), - @ResponseCode(code = 500, condition = "internal server error") - }) + @Operation(summary = "Git configuration", description = "Returns the global git configuration.", tags = "Git") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = GitVndMediaType.GIT_CONFIG, + schema = @Schema(implementation = GitConfigDto.class) + ) + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:read:git\" privilege") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response get() { GitConfig config = repositoryHandler.getConfig(); @@ -80,7 +100,20 @@ public class GitConfigResource { @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:write:git\" privilege"), @ResponseCode(code = 500, condition = "internal server error") }) - @TypeHint(TypeHint.NO_CONTENT.class) + @Operation(summary = "Modify git configuration", description = "Modifies the global git configuration.", tags = "Git") + @ApiResponse( + responseCode = "204", + description = "update success" + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:write:git\" privilege") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response update(GitConfigDto configDto) { GitConfig config = dtoToConfigMapper.map(configDto); @@ -94,7 +127,7 @@ public class GitConfigResource { } @Path("{namespace}/{name}") - public GitRepositoryConfigResource getRepositoryConfig(@PathParam("namespace") String namespace, @PathParam("name") String name) { + public GitRepositoryConfigResource getRepositoryConfig() { return gitRepositoryConfigResource.get(); } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigResource.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigResource.java index 175caf8840..88a9c5d669 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigResource.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigResource.java @@ -2,6 +2,10 @@ package sonia.scm.api.v2.resources; import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.repository.GitRepositoryConfig; @@ -11,6 +15,7 @@ import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryPermissions; import sonia.scm.store.ConfigurationStore; import sonia.scm.web.GitVndMediaType; +import sonia.scm.web.VndMediaType; import javax.inject.Inject; import javax.ws.rs.Consumes; @@ -42,13 +47,31 @@ public class GitRepositoryConfigResource { @GET @Path("/") @Produces(GitVndMediaType.GIT_REPOSITORY_CONFIG) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the repository config"), - @ResponseCode(code = 404, condition = "not found, no repository with the specified namespace and name available"), - @ResponseCode(code = 500, condition = "internal server error") - }) + @Operation(summary = "Git repository configuration", description = "Returns the repository related git configuration.", tags = "Git") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = GitVndMediaType.GIT_REPOSITORY_CONFIG, + schema = @Schema(implementation = GitRepositoryConfigDto.class) + ) + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the repository config") + @ApiResponse( + responseCode = "404", + description = "not found, no repository with the specified namespace and name available", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response getRepositoryConfig(@PathParam("namespace") String namespace, @PathParam("name") String name) { Repository repository = getRepository(namespace, name); RepositoryPermissions.read(repository).check(); @@ -61,13 +84,27 @@ public class GitRepositoryConfigResource { @PUT @Path("/") @Consumes(GitVndMediaType.GIT_REPOSITORY_CONFIG) - @StatusCodes({ - @ResponseCode(code = 204, condition = "update success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the privilege to change this repositories config"), - @ResponseCode(code = 404, condition = "not found, no repository with the specified namespace and name available/name available"), - @ResponseCode(code = 500, condition = "internal server error") - }) + @Operation(summary = "Modifies git repository configuration", description = "Modifies the repository related git configuration.", tags = "Git") + @ApiResponse( + responseCode = "204", + description = "update success" + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the privilege to change this repositories config") + @ApiResponse( + responseCode = "404", + description = "not found, no repository with the specified namespace and name available/name available", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response setRepositoryConfig(@PathParam("namespace") String namespace, @PathParam("name") String name, GitRepositoryConfigDto dto) { Repository repository = getRepository(namespace, name); RepositoryPermissions.custom("git", repository).check(); diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index 4520700dbc..d8068f1bb8 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -481,7 +481,6 @@ io.openapitools.swagger swagger-maven-plugin - 2.1.2 sonia.scm.api.v2.resources From 1ff0c46b0e82193da35c5598365fab815dee81d8 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Tue, 18 Feb 2020 10:13:01 +0100 Subject: [PATCH 08/46] add security requirements --- .../main/java/sonia/scm/api/v2/resources/IndexResource.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexResource.java index 1eec99ea96..611fba6f2c 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexResource.java @@ -1,6 +1,8 @@ package sonia.scm.api.v2.resources; import com.webcohesion.enunciate.metadata.rs.TypeHint; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; import sonia.scm.security.AllowAnonymousAccess; import sonia.scm.web.VndMediaType; @@ -9,6 +11,10 @@ import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; +@OpenAPIDefinition(security = { + @SecurityRequirement(name = "Basic Authentication"), + @SecurityRequirement(name = "Bearer Token Authentication") +}) @Path(IndexResource.INDEX_PATH_V2) @AllowAnonymousAccess public class IndexResource { From 61d7d5e6dd97d399fa23e79bdca2a789f64cd3e2 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Tue, 18 Feb 2020 13:10:37 +0100 Subject: [PATCH 09/46] Add footer extension points for links and avatar --- CHANGELOG.md | 4 ++ .../ui-components/src/avatar/AvatarImage.tsx | 15 +++--- scm-ui/ui-components/src/layout/Footer.tsx | 51 +++++++++++++++++-- scm-ui/ui-styles/src/scm.scss | 11 ++-- scm-ui/ui-webapp/src/containers/App.tsx | 10 ++-- 5 files changed, 69 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4dcc480d1..4bb851e876 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ 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). +## Unreleased +### Added +- Added footer extension points for links and avatar + ## 2.0.0-rc4 - 2020-02-14 ### Added - Support for Java versions > 8 diff --git a/scm-ui/ui-components/src/avatar/AvatarImage.tsx b/scm-ui/ui-components/src/avatar/AvatarImage.tsx index 48808b582f..a30a999d18 100644 --- a/scm-ui/ui-components/src/avatar/AvatarImage.tsx +++ b/scm-ui/ui-components/src/avatar/AvatarImage.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, {FC} from "react"; import { binder } from "@scm-manager/ui-extensions"; import { Image } from ".."; import { Person } from "./Avatar"; @@ -6,21 +6,20 @@ import { EXTENSION_POINT } from "./Avatar"; type Props = { person: Person; + representation?: "rounded" | "rounded-border"; }; -class AvatarImage extends React.Component { - render() { - const { person } = this.props; - +const AvatarImage:FC = ({person, representation = "rounded-border"}) => { const avatarFactory = binder.getExtension(EXTENSION_POINT); if (avatarFactory) { const avatar = avatarFactory(person); - return {person.name}; + const className = representation === "rounded" ? "is-rounded" : "has-rounded-border"; + + return {person.name}; } return null; - } -} +}; export default AvatarImage; diff --git a/scm-ui/ui-components/src/layout/Footer.tsx b/scm-ui/ui-components/src/layout/Footer.tsx index 6c56061a71..2761268a1c 100644 --- a/scm-ui/ui-components/src/layout/Footer.tsx +++ b/scm-ui/ui-components/src/layout/Footer.tsx @@ -1,23 +1,66 @@ import React from "react"; -import { Me } from "@scm-manager/ui-types"; +import { Me, Links } from "@scm-manager/ui-types"; import { Link } from "react-router-dom"; +import { binder } from "@scm-manager/ui-extensions"; +import { AvatarWrapper, AvatarImage } from "../avatar"; type Props = { me?: Me; + version: string; + links: Links; }; +const RestDocLink = () => { + return Rest Documentation; +}; + +binder.bind("footer.links", RestDocLink); + class Footer extends React.Component { render() { - const { me } = this.props; + const { me, version, links } = this.props; if (!me) { return ""; } + + const extensions = binder.getExtensions("footer.links", { me, links }); + return ( ); diff --git a/scm-ui/ui-styles/src/scm.scss b/scm-ui/ui-styles/src/scm.scss index cca6a484ed..dffa4cb00e 100644 --- a/scm-ui/ui-styles/src/scm.scss +++ b/scm-ui/ui-styles/src/scm.scss @@ -67,8 +67,10 @@ hr.header-with-actions { } } -.footer { - height: 50px; +footer.footer { + //height: 100px; + background-color: whitesmoke; + padding: 2rem 1.5rem 1rem; } // 6. Import the rest of Bulma @@ -691,11 +693,6 @@ form .field:not(.is-grouped) { } } -// footer -.footer { - background-color: whitesmoke; -} - // aside .aside-background { bottom: 0; diff --git a/scm-ui/ui-webapp/src/containers/App.tsx b/scm-ui/ui-webapp/src/containers/App.tsx index 281e47e920..6170621c65 100644 --- a/scm-ui/ui-webapp/src/containers/App.tsx +++ b/scm-ui/ui-webapp/src/containers/App.tsx @@ -8,6 +8,7 @@ import { fetchMe, getFetchMeFailure, getMe, isAuthenticated, isFetchMePending } import { ErrorPage, Footer, Header, Loading, PrimaryNavigation } from "@scm-manager/ui-components"; import { Links, Me } from "@scm-manager/ui-types"; import { + getAppVersion, getFetchIndexResourcesFailure, getLinks, getMeLink, @@ -21,6 +22,7 @@ type Props = WithTranslation & { loading: boolean; links: Links; meLink: string; + version: string; // dispatcher functions fetchMe: (link: string) => void; @@ -34,7 +36,7 @@ class App extends Component { } render() { - const { me, loading, error, authenticated, links, t } = this.props; + const { me, loading, error, authenticated, links, version, t } = this.props; let content; const navigation = authenticated ? : ""; @@ -50,7 +52,7 @@ class App extends Component {
{navigation}
{content} - {authenticated &&
} + {authenticated &&
}
); } @@ -69,13 +71,15 @@ const mapStateToProps = (state: any) => { const error = getFetchMeFailure(state) || getFetchIndexResourcesFailure(state); const links = getLinks(state); const meLink = getMeLink(state); + const version = getAppVersion(state); return { authenticated, me, loading, error, links, - meLink + meLink, + version }; }; From 6431f19b3089e21335353a725c6c7d313a46d6af Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Tue, 18 Feb 2020 14:03:33 +0100 Subject: [PATCH 10/46] rm link mock --- scm-ui/ui-components/src/layout/Footer.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/scm-ui/ui-components/src/layout/Footer.tsx b/scm-ui/ui-components/src/layout/Footer.tsx index 2761268a1c..63e294bb01 100644 --- a/scm-ui/ui-components/src/layout/Footer.tsx +++ b/scm-ui/ui-components/src/layout/Footer.tsx @@ -10,12 +10,6 @@ type Props = { links: Links; }; -const RestDocLink = () => { - return Rest Documentation; -}; - -binder.bind("footer.links", RestDocLink); - class Footer extends React.Component { render() { const { me, version, links } = this.props; @@ -56,7 +50,7 @@ class Footer extends React.Component { Cloudogu GmbH {" "} - | Learn more at{" "} + | Learn more about{" "} SCM-Manager From e53629e1525e4a9d977ccdcd3693b4a9cf078fbc Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 18 Feb 2020 14:40:43 +0100 Subject: [PATCH 11/46] remove jwt libraries from scm-hg-plugin --- scm-plugins/scm-hg-plugin/pom.xml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/scm-plugins/scm-hg-plugin/pom.xml b/scm-plugins/scm-hg-plugin/pom.xml index 2dfd89c8fb..c89d7a085d 100644 --- a/scm-plugins/scm-hg-plugin/pom.xml +++ b/scm-plugins/scm-hg-plugin/pom.xml @@ -28,18 +28,6 @@ - - io.jsonwebtoken - jjwt-impl - 0.10.5 - provided - - - io.jsonwebtoken - jjwt-jackson - 0.10.5 - test - From d8249609208e3521ce59cc1c1c83308c4387ae03 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 18 Feb 2020 13:42:31 +0000 Subject: [PATCH 12/46] Close branch bugfix/hg_edit_with_xsrf From 5364e8682ddd54fbaa8f51c673deca7464a0994a Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Tue, 18 Feb 2020 17:19:35 +0100 Subject: [PATCH 13/46] Create openapi docs for hg/svn plugin --- .../api/v2/resources/GitConfigResource.java | 9 --- .../HgConfigAutoConfigurationResource.java | 50 ++++++++++------ .../HgConfigInstallationsResource.java | 58 ++++++++++++------ .../v2/resources/HgConfigPackageResource.java | 60 +++++++++++++------ .../api/v2/resources/HgConfigResource.java | 60 +++++++++++++------ .../api/v2/resources/SvnConfigResource.java | 59 ++++++++++++------ 6 files changed, 199 insertions(+), 97 deletions(-) diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java index d3332d5a37..098396098f 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java @@ -1,8 +1,5 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -94,12 +91,6 @@ public class GitConfigResource { @PUT @Path("") @Consumes(GitVndMediaType.GIT_CONFIG) - @StatusCodes({ - @ResponseCode(code = 204, condition = "update success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:write:git\" privilege"), - @ResponseCode(code = 500, condition = "internal server error") - }) @Operation(summary = "Modify git configuration", description = "Modifies the global git configuration.", tags = "Git") @ApiResponse( responseCode = "204", diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigAutoConfigurationResource.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigAutoConfigurationResource.java index b265f2929d..3507c41da3 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigAutoConfigurationResource.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigAutoConfigurationResource.java @@ -1,13 +1,15 @@ package sonia.scm.api.v2.resources; import com.google.inject.Inject; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import sonia.scm.config.ConfigurationPermissions; import sonia.scm.repository.HgConfig; import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.web.HgVndMediaType; +import sonia.scm.web.VndMediaType; import javax.ws.rs.Consumes; import javax.ws.rs.PUT; @@ -31,13 +33,20 @@ public class HgConfigAutoConfigurationResource { */ @PUT @Path("") - @StatusCodes({ - @ResponseCode(code = 204, condition = "update success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:write:hg\" privilege"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) + @Operation(summary = "Sets hg configuration and installs hg binary", description = "Sets the default mercurial config and installs the mercurial binary.", tags = "Mercurial") + @ApiResponse( + responseCode = "204", + description = "update success" + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:write:hg\" privilege") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response autoConfiguration() { return autoConfiguration(null); } @@ -50,13 +59,20 @@ public class HgConfigAutoConfigurationResource { @PUT @Path("") @Consumes(HgVndMediaType.CONFIG) - @StatusCodes({ - @ResponseCode(code = 204, condition = "update success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:write:hg\" privilege"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) + @Operation(summary = "Modifies hg configuration and installs hg binary", description = "Modifies the mercurial config and installs the mercurial binary.", tags = "Mercurial") + @ApiResponse( + responseCode = "204", + description = "update success" + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:write:hg\" privilege") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response autoConfiguration(HgConfigDto configDto) { HgConfig config; diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsResource.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsResource.java index 8842d07569..795d0c87a6 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsResource.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsResource.java @@ -1,13 +1,15 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; import de.otto.edison.hal.HalRepresentation; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import sonia.scm.config.ConfigurationPermissions; import sonia.scm.installer.HgInstallerFactory; import sonia.scm.repository.HgConfig; import sonia.scm.web.HgVndMediaType; +import sonia.scm.web.VndMediaType; import javax.inject.Inject; import javax.ws.rs.GET; @@ -31,13 +33,24 @@ public class HgConfigInstallationsResource { @GET @Path(PATH_HG) @Produces(HgVndMediaType.INSTALLATIONS) - @TypeHint(HalRepresentation.class) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:read:hg\" privilege"), - @ResponseCode(code = 500, condition = "internal server error") - }) + @Operation(summary = "Hg installations", description = "Returns the mercurial installations.", tags = "Mercurial") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = HgVndMediaType.INSTALLATIONS, + schema = @Schema(implementation = HgConfigInstallationsDto.class) + ) + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:read:hg\" privilege") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public HalRepresentation getHgInstallations() { ConfigurationPermissions.read(HgConfig.PERMISSION).check(); @@ -52,13 +65,24 @@ public class HgConfigInstallationsResource { @GET @Path(PATH_PYTHON) @Produces(HgVndMediaType.INSTALLATIONS) - @TypeHint(HalRepresentation.class) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:read:hg\" privilege"), - @ResponseCode(code = 500, condition = "internal server error") - }) + @Operation(summary = "Python installations", description = "Returns the python installations.", tags = "Mercurial") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = HgVndMediaType.INSTALLATIONS, + schema = @Schema(implementation = HgConfigInstallationsDto.class) + ) + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:read:hg\" privilege") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public HalRepresentation getPythonInstallations() { ConfigurationPermissions.read(HgConfig.PERMISSION).check(); diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackageResource.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackageResource.java index 88a7de7ea0..2e185f152d 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackageResource.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackageResource.java @@ -1,9 +1,10 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; import de.otto.edison.hal.HalRepresentation; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import sonia.scm.SCMContext; import sonia.scm.config.ConfigurationPermissions; import sonia.scm.installer.HgInstallerFactory; @@ -13,6 +14,7 @@ import sonia.scm.net.ahc.AdvancedHttpClient; import sonia.scm.repository.HgConfig; import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.web.HgVndMediaType; +import sonia.scm.web.VndMediaType; import javax.inject.Inject; import javax.ws.rs.GET; @@ -44,13 +46,20 @@ public class HgConfigPackageResource { @GET @Path("") @Produces(HgVndMediaType.PACKAGES) - @StatusCodes({ - @ResponseCode(code = 204, condition = "update success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:read:hg\" privilege"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(HalRepresentation.class) + @Operation(summary = "Hg configuration packages", description = "Returns all mercurial packages.", tags = "Mercurial") + @ApiResponse( + responseCode = "204", + description = "update success" + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:read:hg\" privilege") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public HalRepresentation getPackages() { ConfigurationPermissions.read(HgConfig.PERMISSION).check(); @@ -65,14 +74,27 @@ public class HgConfigPackageResource { */ @PUT @Path("{pkgId}") - @StatusCodes({ - @ResponseCode(code = 204, condition = "update success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:write:hg\" privilege"), - @ResponseCode(code = 404, condition = "no package found for id"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) + @Operation(summary = "Modifies hg configuration package", description = "Installs a mercurial package.", tags = "Mercurial") + @ApiResponse( + responseCode = "204", + description = "update success" + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:write:hg\" privilege") + @ApiResponse( + responseCode = "404", + description = "no package found for id", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response installPackage(@PathParam("pkgId") String pkgId) { Response response; @@ -82,7 +104,7 @@ public class HgConfigPackageResource { if (pkg != null) { if (HgInstallerFactory.createInstaller() - .installPackage(client, handler, SCMContext.getContext().getBaseDirectory(), pkg)) { + .installPackage(client, handler, SCMContext.getContext().getBaseDirectory(), pkg)) { response = Response.noContent().build(); } else { response = Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigResource.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigResource.java index e6a8f01238..be534f4345 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigResource.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigResource.java @@ -1,12 +1,16 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import sonia.scm.config.ConfigurationPermissions; import sonia.scm.repository.HgConfig; import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.web.HgVndMediaType; +import sonia.scm.web.VndMediaType; import javax.inject.Inject; import javax.inject.Provider; @@ -20,11 +24,13 @@ import javax.ws.rs.core.Response; /** * RESTful Web Service Resource to manage the configuration of the hg plugin. */ +@OpenAPIDefinition(tags = { + @Tag(name = "Mercurial", description = "Configuration for the mercurial repository type") +}) @Path(HgConfigResource.HG_CONFIG_PATH_V2) public class HgConfigResource { static final String HG_CONFIG_PATH_V2 = "v2/config/hg"; - private final HgConfigDtoToHgConfigMapper dtoToConfigMapper; private final HgConfigToHgConfigDtoMapper configToDtoMapper; private final HgRepositoryHandler repositoryHandler; @@ -51,13 +57,24 @@ public class HgConfigResource { @GET @Path("") @Produces(HgVndMediaType.CONFIG) - @TypeHint(HgConfigDto.class) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:read:hg\" privilege"), - @ResponseCode(code = 500, condition = "internal server error") - }) + @Operation(summary = "Hg configuration", description = "Returns the global mercurial configuration.", tags = "Mercurial") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = HgVndMediaType.CONFIG, + schema = @Schema(implementation = HgConfigDto.class) + ) + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:read:hg\" privilege") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response get() { ConfigurationPermissions.read(HgConfig.PERMISSION).check(); @@ -80,13 +97,20 @@ public class HgConfigResource { @PUT @Path("") @Consumes(HgVndMediaType.CONFIG) - @StatusCodes({ - @ResponseCode(code = 204, condition = "update success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:write:hg\" privilege"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) + @Operation(summary = "Modify hg configuration", description = "Modifies the global mercurial configuration.", tags = "Mercurial") + @ApiResponse( + responseCode = "204", + description = "update success" + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:write:hg\" privilege") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response update(HgConfigDto configDto) { HgConfig config = dtoToConfigMapper.map(configDto); diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigResource.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigResource.java index b12785dca9..9ff13ffb46 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigResource.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigResource.java @@ -1,12 +1,16 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import sonia.scm.config.ConfigurationPermissions; import sonia.scm.repository.SvnConfig; import sonia.scm.repository.SvnRepositoryHandler; import sonia.scm.web.SvnVndMediaType; +import sonia.scm.web.VndMediaType; import javax.inject.Inject; import javax.ws.rs.Consumes; @@ -19,6 +23,9 @@ import javax.ws.rs.core.Response; /** * RESTful Web Service Resource to manage the configuration of the svn plugin. */ +@OpenAPIDefinition(tags = { + @Tag(name = "Subversion", description = "Configuration for the subversion repository type") +}) @Path(SvnConfigResource.SVN_CONFIG_PATH_V2) public class SvnConfigResource { @@ -41,13 +48,24 @@ public class SvnConfigResource { @GET @Path("") @Produces(SvnVndMediaType.SVN_CONFIG) - @TypeHint(SvnConfigDto.class) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:read:svn\" privilege"), - @ResponseCode(code = 500, condition = "internal server error") - }) + @Operation(summary = "Svn configuration", description = "Returns the global subversion configuration.", tags = "Subversion") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = SvnVndMediaType.SVN_CONFIG, + schema = @Schema(implementation = SvnConfigDto.class) + ) + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:read:svn\" privilege") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response get() { SvnConfig config = repositoryHandler.getConfig(); @@ -70,13 +88,20 @@ public class SvnConfigResource { @PUT @Path("") @Consumes(SvnVndMediaType.SVN_CONFIG) - @StatusCodes({ - @ResponseCode(code = 204, condition = "update success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:write:svn\" privilege"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) + @Operation(summary = "Modify svn configuration", description = "Modifies the global subversion configuration.", tags = "Subversion") + @ApiResponse( + responseCode = "204", + description = "update success" + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:write:svn\" privilege") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response update(SvnConfigDto configDto) { SvnConfig config = dtoToConfigMapper.map(configDto); From dc83b50095d844e89af78f75eef7494fbb747a0c Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Tue, 18 Feb 2020 17:20:23 +0100 Subject: [PATCH 14/46] Create openapi docs for me endpoint and changesets --- .../v2/resources/ChangesetRootResource.java | 119 +++++++++++------- .../v2/resources/FileHistoryRootResource.java | 40 ++++-- .../scm/api/v2/resources/MeResource.java | 49 ++++++-- 3 files changed, 142 insertions(+), 66 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetRootResource.java index 0766816d4d..e7338b42f2 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetRootResource.java @@ -1,8 +1,9 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import lombok.extern.slf4j.Slf4j; import sonia.scm.PageResult; import sonia.scm.repository.Changeset; @@ -42,17 +43,82 @@ public class ChangesetRootResource { this.changesetToChangesetDtoMapper = changesetToChangesetDtoMapper; } + @GET + @Path("{id}") + @Produces(VndMediaType.CHANGESET) + @Operation(summary = "Collection of changesets", description = "Returns a collection of changesets.", tags = "Repository") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = VndMediaType.CHANGESET, + schema = @Schema(implementation = ChangesetDto.class) + ) + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the changeset") + @ApiResponse( + responseCode = "404", + description = "not found, no changeset with the specified id is available in the repository", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("id") String id) throws IOException { + try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { + Repository repository = repositoryService.getRepository(); + RepositoryPermissions.read(repository).check(); + ChangesetPagingResult changesets = repositoryService.getLogCommand() + .setStartChangeset(id) + .setEndChangeset(id) + .getChangesets(); + if (changesets != null && changesets.getChangesets() != null && !changesets.getChangesets().isEmpty()) { + Optional changeset = changesets.getChangesets().stream().filter(ch -> ch.getId().equals(id)).findFirst(); + if (!changeset.isPresent()) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + return Response.ok(changesetToChangesetDtoMapper.map(changeset.get(), repository)).build(); + } else { + return Response.status(Response.Status.NOT_FOUND).build(); + } + } + } + @GET @Path("") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the changeset"), - @ResponseCode(code = 404, condition = "not found, no changesets available in the repository"), - @ResponseCode(code = 500, condition = "internal server error") - }) @Produces(VndMediaType.CHANGESET_COLLECTION) - @TypeHint(CollectionDto.class) + @Operation(summary = "Specific changeset", description = "Returns a specific changeset.", tags = "Repository") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = VndMediaType.CHANGESET_COLLECTION, + schema = @Schema(implementation = CollectionDto.class) + ) + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the changeset") + @ApiResponse( + responseCode = "404", + description = "not found, no changesets available in the repository", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name, @DefaultValue("0") @QueryParam("page") int page, @DefaultValue("10") @QueryParam("pageSize") int pageSize) throws IOException { try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { @@ -75,35 +141,4 @@ public class ChangesetRootResource { } } } - - @GET - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the changeset"), - @ResponseCode(code = 404, condition = "not found, no changeset with the specified id is available in the repository"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @Produces(VndMediaType.CHANGESET) - @TypeHint(ChangesetDto.class) - @Path("{id}") - public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("id") String id) throws IOException { - try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { - Repository repository = repositoryService.getRepository(); - RepositoryPermissions.read(repository).check(); - ChangesetPagingResult changesets = repositoryService.getLogCommand() - .setStartChangeset(id) - .setEndChangeset(id) - .getChangesets(); - if (changesets != null && changesets.getChangesets() != null && !changesets.getChangesets().isEmpty()) { - Optional changeset = changesets.getChangesets().stream().filter(ch -> ch.getId().equals(id)).findFirst(); - if (!changeset.isPresent()) { - return Response.status(Response.Status.NOT_FOUND).build(); - } - return Response.ok(changesetToChangesetDtoMapper.map(changeset.get(), repository)).build(); - } else { - return Response.status(Response.Status.NOT_FOUND).build(); - } - } - } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryRootResource.java index 5e087dc7ca..3a66f9c7a1 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryRootResource.java @@ -1,8 +1,9 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import lombok.extern.slf4j.Slf4j; import sonia.scm.PageResult; import sonia.scm.repository.Changeset; @@ -54,15 +55,32 @@ public class FileHistoryRootResource { */ @GET @Path("{revision}/{path: .*}") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the changeset"), - @ResponseCode(code = 404, condition = "not found, no changesets available in the repository"), - @ResponseCode(code = 500, condition = "internal server error") - }) @Produces(VndMediaType.CHANGESET_COLLECTION) - @TypeHint(CollectionDto.class) + @Operation(summary = "Changesets to given file", description = "Get all changesets related to the given file starting with the given revision.", tags = "Repository") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = VndMediaType.CHANGESET_COLLECTION, + schema = @Schema(implementation = CollectionDto.class) + ) + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the changeset") + @ApiResponse( + responseCode = "404", + description = "not found, no changesets available in the repository", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path, diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java index 2c2e208893..eae947c5d8 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java @@ -3,6 +3,12 @@ package sonia.scm.api.v2.resources; import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import org.apache.shiro.authc.credential.PasswordService; import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; @@ -23,11 +29,13 @@ import javax.ws.rs.core.UriInfo; /** * RESTful Web Service Resource to get currently logged in users. */ +@OpenAPIDefinition(tags = { + @Tag(name = "Me", description = "Me related endpoints") +}) @Path(MeResource.ME_PATH_V2) public class MeResource { static final String ME_PATH_V2 = "v2/me/"; - private final MeDtoFactory meDtoFactory; private final UserManager userManager; private final PasswordService passwordService; @@ -45,12 +53,23 @@ public class MeResource { @GET @Path("") @Produces(VndMediaType.ME) - @TypeHint(MeDto.class) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 500, condition = "internal server error") - }) + @Operation(summary = "Current user", description = "Returns the currently logged in user or a 401 if user is not logged in.", tags = "Me") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = VndMediaType.ME, + schema = @Schema(implementation = MeDto.class) + ) + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response get(@Context Request request, @Context UriInfo uriInfo) { return Response.ok(meDtoFactory.create()).build(); } @@ -60,13 +79,17 @@ public class MeResource { */ @PUT @Path("password") - @StatusCodes({ - @ResponseCode(code = 204, condition = "update success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) @Consumes(VndMediaType.PASSWORD_CHANGE) + @Operation(summary = "Change password", description = "Change password of the current user.", tags = "Me") + @ApiResponse(responseCode = "204", description = "update success") + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response changePassword(@Valid PasswordChangeDto passwordChange) { userManager.changePasswordForLoggedInUser( passwordService.encryptPassword(passwordChange.getOldPassword()), From 3f9d61ca99488f6b47a8568a667cc543a26e6a58 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 19 Feb 2020 08:48:51 +0100 Subject: [PATCH 15/46] inject props to each extension --- scm-ui/ui-components/src/layout/Footer.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scm-ui/ui-components/src/layout/Footer.tsx b/scm-ui/ui-components/src/layout/Footer.tsx index 63e294bb01..a74551198a 100644 --- a/scm-ui/ui-components/src/layout/Footer.tsx +++ b/scm-ui/ui-components/src/layout/Footer.tsx @@ -17,7 +17,8 @@ class Footer extends React.Component { return ""; } - const extensions = binder.getExtensions("footer.links", { me, links }); + const extensionProps = { me, links }; + const extensions = binder.getExtensions("footer.links", extensionProps); return (
@@ -40,7 +41,7 @@ class Footer extends React.Component { {extensions.map(Ext => ( <> {" "} - | + | ))}

From fac46d636f38d9a834654c3269594a652122c381 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 19 Feb 2020 09:46:57 +0100 Subject: [PATCH 16/46] fixed wrong typing of me groups --- scm-ui/ui-types/src/Me.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui/ui-types/src/Me.ts b/scm-ui/ui-types/src/Me.ts index d43932d9b3..9f478a9ccc 100644 --- a/scm-ui/ui-types/src/Me.ts +++ b/scm-ui/ui-types/src/Me.ts @@ -4,6 +4,6 @@ export type Me = { name: string; displayName: string; mail: string; - groups: []; + groups: string[]; _links: Links; }; From 041a999a01d52f04fabfc490d058e85332068b92 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 19 Feb 2020 09:47:52 +0100 Subject: [PATCH 17/46] added context hook to allow the override of the default binder --- scm-ui/ui-extensions/src/binder.ts | 6 +++-- scm-ui/ui-extensions/src/index.ts | 3 ++- scm-ui/ui-extensions/src/useBinder.test.tsx | 29 +++++++++++++++++++++ scm-ui/ui-extensions/src/useBinder.ts | 16 ++++++++++++ 4 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 scm-ui/ui-extensions/src/useBinder.test.tsx create mode 100644 scm-ui/ui-extensions/src/useBinder.ts diff --git a/scm-ui/ui-extensions/src/binder.ts b/scm-ui/ui-extensions/src/binder.ts index a359973a50..e3f2b9f120 100644 --- a/scm-ui/ui-extensions/src/binder.ts +++ b/scm-ui/ui-extensions/src/binder.ts @@ -10,11 +10,13 @@ type ExtensionRegistration = { * The Binder class is mainly exported for testing, plugins should only use the default export. */ export class Binder { + name: string; extensionPoints: { [key: string]: Array; }; - constructor() { + constructor(name: string) { + this.name = name; this.extensionPoints = {}; } @@ -73,6 +75,6 @@ export class Binder { } // singleton binder -const binder = new Binder(); +const binder = new Binder("default"); export default binder; diff --git a/scm-ui/ui-extensions/src/index.ts b/scm-ui/ui-extensions/src/index.ts index 73267e29a3..1c632b8a38 100644 --- a/scm-ui/ui-extensions/src/index.ts +++ b/scm-ui/ui-extensions/src/index.ts @@ -1,2 +1,3 @@ -export { default as binder } from "./binder"; +export { default as binder, Binder } from "./binder"; +export * from "./useBinder"; export { default as ExtensionPoint } from "./ExtensionPoint"; diff --git a/scm-ui/ui-extensions/src/useBinder.test.tsx b/scm-ui/ui-extensions/src/useBinder.test.tsx new file mode 100644 index 0000000000..4fc73974d6 --- /dev/null +++ b/scm-ui/ui-extensions/src/useBinder.test.tsx @@ -0,0 +1,29 @@ +import useBinder, { BinderContext } from "./useBinder"; +import { Binder } from "./binder"; +import { mount } from "enzyme"; +import "@scm-manager/ui-tests/enzyme"; +import React from "react"; + +describe("useBinder tests", () => { + const BinderName = () => { + const binder = useBinder(); + return <>{binder.name}; + }; + + it("should return default binder", () => { + const rendered = mount(); + expect(rendered.text()).toBe("default"); + }); + + it("should return binder from context", () => { + const binder = new Binder("from-context"); + const app = ( + + + + ); + + const rendered = mount(app); + expect(rendered.text()).toBe("from-context"); + }); +}); diff --git a/scm-ui/ui-extensions/src/useBinder.ts b/scm-ui/ui-extensions/src/useBinder.ts new file mode 100644 index 0000000000..1d63dd7e8d --- /dev/null +++ b/scm-ui/ui-extensions/src/useBinder.ts @@ -0,0 +1,16 @@ +import { createContext, useContext } from "react"; +import defaultBinder from "./binder"; + +/** + * The BinderContext should only be used to override the default binder for testing purposes. + */ +export const BinderContext = createContext(defaultBinder); + +/** + * Hook to get the binder from context. + */ +export const useBinder = () => { + return useContext(BinderContext); +}; + +export default useBinder; From f23b4645246ff43222a37910c18dc74667bb3f49 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 19 Feb 2020 09:48:46 +0100 Subject: [PATCH 18/46] use binder from context to make extensions it testable --- .../ui-components/src/avatar/AvatarImage.tsx | 21 ++-- .../src/avatar/AvatarWrapper.tsx | 21 ++-- scm-ui/ui-components/src/layout/Footer.tsx | 101 +++++++++--------- 3 files changed, 68 insertions(+), 75 deletions(-) diff --git a/scm-ui/ui-components/src/avatar/AvatarImage.tsx b/scm-ui/ui-components/src/avatar/AvatarImage.tsx index a30a999d18..d85d2279ee 100644 --- a/scm-ui/ui-components/src/avatar/AvatarImage.tsx +++ b/scm-ui/ui-components/src/avatar/AvatarImage.tsx @@ -1,25 +1,26 @@ -import React, {FC} from "react"; -import { binder } from "@scm-manager/ui-extensions"; +import React, { FC } from "react"; import { Image } from ".."; import { Person } from "./Avatar"; import { EXTENSION_POINT } from "./Avatar"; +import { useBinder } from "@scm-manager/ui-extensions"; type Props = { person: Person; representation?: "rounded" | "rounded-border"; }; -const AvatarImage:FC = ({person, representation = "rounded-border"}) => { - const avatarFactory = binder.getExtension(EXTENSION_POINT); - if (avatarFactory) { - const avatar = avatarFactory(person); +const AvatarImage: FC = ({ person, representation = "rounded-border" }) => { + const binder = useBinder(); + const avatarFactory = binder.getExtension(EXTENSION_POINT); + if (avatarFactory) { + const avatar = avatarFactory(person); - const className = representation === "rounded" ? "is-rounded" : "has-rounded-border"; + const className = representation === "rounded" ? "is-rounded" : "has-rounded-border"; - return {person.name}; - } + return {person.name}; + } - return null; + return null; }; export default AvatarImage; diff --git a/scm-ui/ui-components/src/avatar/AvatarWrapper.tsx b/scm-ui/ui-components/src/avatar/AvatarWrapper.tsx index 09ed7391cf..695cb36064 100644 --- a/scm-ui/ui-components/src/avatar/AvatarWrapper.tsx +++ b/scm-ui/ui-components/src/avatar/AvatarWrapper.tsx @@ -1,18 +1,13 @@ -import React, { Component, ReactNode } from "react"; -import { binder } from "@scm-manager/ui-extensions"; +import React, { FC } from "react"; +import { useBinder } from "@scm-manager/ui-extensions"; import { EXTENSION_POINT } from "./Avatar"; -type Props = { - children: ReactNode; +const AvatarWrapper: FC = ({ children }) => { + const binder = useBinder(); + if (binder.hasExtension(EXTENSION_POINT)) { + return <>{children}; + } + return null; }; -class AvatarWrapper extends Component { - render() { - if (binder.hasExtension(EXTENSION_POINT)) { - return <>{this.props.children}; - } - return null; - } -} - export default AvatarWrapper; diff --git a/scm-ui/ui-components/src/layout/Footer.tsx b/scm-ui/ui-components/src/layout/Footer.tsx index a74551198a..3243134353 100644 --- a/scm-ui/ui-components/src/layout/Footer.tsx +++ b/scm-ui/ui-components/src/layout/Footer.tsx @@ -1,7 +1,7 @@ -import React from "react"; +import React, { FC } from "react"; import { Me, Links } from "@scm-manager/ui-types"; import { Link } from "react-router-dom"; -import { binder } from "@scm-manager/ui-extensions"; +import { useBinder } from "@scm-manager/ui-extensions"; import { AvatarWrapper, AvatarImage } from "../avatar"; type Props = { @@ -10,56 +10,53 @@ type Props = { links: Links; }; -class Footer extends React.Component { - render() { - const { me, version, links } = this.props; - if (!me) { - return ""; - } - - const extensionProps = { me, links }; - const extensions = binder.getExtensions("footer.links", extensionProps); - - return ( - - ); +const Footer: FC = ({ me, version, links }) => { + const binder = useBinder(); + if (!me) { + return null; } -} + const extensionProps = { me, links }; + const extensions = binder.getExtensions("footer.links", extensionProps); + + return ( + + ); +}; export default Footer; From f96ebc98cd12de7a5b61110dbbbea543a1de7f4f Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 19 Feb 2020 09:49:31 +0100 Subject: [PATCH 19/46] added stories for the footer --- .../src/__resources__/avatar.png | Bin 0 -> 72161 bytes .../src/__snapshots__/storyshots.test.ts.snap | 266 ++++++++++++++++++ .../src/layout/Footer.stories.tsx | 56 ++++ 3 files changed, 322 insertions(+) create mode 100644 scm-ui/ui-components/src/__resources__/avatar.png create mode 100644 scm-ui/ui-components/src/layout/Footer.stories.tsx diff --git a/scm-ui/ui-components/src/__resources__/avatar.png b/scm-ui/ui-components/src/__resources__/avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..71632a3a512a08b9385ca36b8e49a9f9f06f72a1 GIT binary patch literal 72161 zcmeEu`CE-`|MjjUk}_0^1~ZLHQf-Y0rFj-ALZga`q)~(n5lw0<(yWPQ4VzSm<^h#P znrNU>X?WMU@9+2h3*H~!`#7HC;lB62_H~`-XIN`})_L93(>b`DeJwkMLRqeHXuko4 z!qQLvw`4K?jdomPFaB?l>s}3`CHTi{$*CLo`%;%fM_lpw7VJMQl6FoGuGS}9PD?tUw}~27TuY&>r)ca~GxCW3*?!T3$GtmuY}@ix z21|$M*~bz`#IsQ#$XZ{rm!>ojilltt=#$*cVJ_VVa2Qd{|q zor!tFM3Ha)4`bgX)oO(f1=ryk_wO5rhO66#2PJjSRNoFR>vCCwTcS|-zB^DEf1pU! zGLb)IE?WHGzu={-vXOs5XXeB6;vX{#iy-3<%dBEq$RDhjINTWjLfp@vll;Mt^?xry z`TrOBf4e4AQDlYXc8G}`E}frAO+QxfG}Sa?aIh&=Q$u6AA*1D=^}=|~+(hu{GiN?@ zbXfZM_3?VwC3+Ef;3yzPz*vjf}LTw!||tGtXamyGHVu*%r-*dSl(?3*$Tj0*=&m z6u+ZCdR*iM5;h@lULFpYocmQf-}Bd|I$Gb{{L#R%U|wF{N#6tiEU$ZIoh*O2=b}?X zGKDfLOnxxBv-kajnfZSs?)|k-ROUR#`}O0WJ&UL=v*g~cxBhZwxQO8a9x)T6YwSEm zStr%oFSMRzU$R7swuCaemwex;HluX@ky6Qm{97(T>a*V7Qw^6~R;j3{gi0JW!53#$ zEaNppLxO{Y2li9^w51W=Qvp@sI~HP6J4+WlyWX%iT#^!(lG6KL6<#{*vBN0o-hd*- z@47XDyU5`D`4<;PzNNi=yZ={5smedisbs@Mt;E;+u3f*Ldj9)|l;f{*?OW2*-n~0y zoMAz$dBs9;;zN2^T{g|IXnL%4`C&vv#GLzRr`qY$rz3R&KCv!Zlw{wO@+F}7!mqC> z#;IX@_wLn7WTJ@QKU?%VbEgNVrKM%)4ol_BnG5p{ z{>-f83moMB6iw{yQzn14Ccb_9_NUtXC%KO8n>LlXt#xV8#2zHp`BVyCOft(lneY

qlz(7I+CA{EQZY#H-U|N5_t*m|HA*_ZRZ4t#~ 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 - 10.16.0 - 1.16.0 + 12.16.1 + 1.22.0 8 diff --git a/scm-ui/ui-components/src/comparators.ts b/scm-ui/ui-components/src/comparators.ts index e4d661c2fd..68ae3d0734 100644 --- a/scm-ui/ui-components/src/comparators.ts +++ b/scm-ui/ui-components/src/comparators.ts @@ -15,7 +15,7 @@ export const byKey = (key: string) => { } if (isUndefined(b, key)) { - return 0; + return -1; } if (a[key] < b[key]) { @@ -35,7 +35,7 @@ export const byValueLength = (key: string) => { } if (isUndefined(b, key)) { - return 0; + return -1; } if (a[key].length < b[key].length) { @@ -55,7 +55,7 @@ export const byNestedKeys = (key: string, nestedKey: string) => { } if (isUndefined(b, key, nestedKey)) { - return 0; + return -1; } if (a[key][nestedKey] < b[key][nestedKey]) { From cbd86e8e5c584ce84560fed226758cffa93b927e Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Wed, 19 Feb 2020 16:51:20 +0100 Subject: [PATCH 28/46] Update rest resource annotations --- .../api/v2/resources/BranchRootResource.java | 123 ++++++++++----- .../v2/resources/ChangesetRootResource.java | 100 ++++++------ .../scm/api/v2/resources/GroupResource.java | 2 +- .../v2/resources/IncomingRootResource.java | 112 ++++++++++---- .../resources/ModificationsRootResource.java | 36 +++-- .../resources/NamespaceStrategyResource.java | 2 + .../RepositoryCollectionResource.java | 54 ++++--- .../RepositoryPermissionRootResource.java | 144 +++++++++++++----- .../api/v2/resources/RepositoryResource.java | 39 +++-- .../RepositoryRoleCollectionResource.java | 54 ++++--- .../v2/resources/RepositoryRoleResource.java | 74 +++++---- .../resources/RepositoryRoleRootResource.java | 6 + .../RepositoryTypeCollectionResource.java | 28 +++- .../v2/resources/RepositoryTypeResource.java | 31 ++-- .../v2/resources/RepositoryVerbResource.java | 26 +++- .../api/v2/resources/SourceRootResource.java | 10 +- .../scm/api/v2/resources/TagRootResource.java | 68 ++++++--- .../api/v2/resources/UIPluginResource.java | 57 +++++-- .../scm/api/v2/resources/UserResource.java | 6 +- 19 files changed, 653 insertions(+), 319 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java index 9e7353b4b7..1a41508e51 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java @@ -1,11 +1,12 @@ package sonia.scm.api.v2.resources; import com.google.common.base.Strings; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.ResponseHeader; import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import sonia.scm.PageResult; import sonia.scm.repository.Branch; import sonia.scm.repository.Branches; @@ -69,15 +70,27 @@ public class BranchRootResource { @GET @Path("{branch}") @Produces(VndMediaType.BRANCH) - @TypeHint(BranchDto.class) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 400, condition = "branches not supported for given repository"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the branch"), - @ResponseCode(code = 404, condition = "not found, no branch with the specified name for the repository available or repository not found"), - @ResponseCode(code = 500, condition = "internal server error") - }) + @Operation(summary = "Get single branch", description = "Returns a branch for a repository.", tags = "Repository") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = VndMediaType.BRANCH, + schema = @Schema(implementation = BranchDto.class) + ) + ) + @ApiResponse(responseCode = "400", description = "branches not supported for given repository") + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the branch") + @ApiResponse(responseCode = "404", description = "not found, no branch with the specified name for the repository available or repository not found") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + ) + ) public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("branch") String branchName) throws IOException { NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name); try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) { @@ -95,17 +108,29 @@ public class BranchRootResource { } } - @Path("{branch}/changesets/") @GET - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the changeset"), - @ResponseCode(code = 404, condition = "not found, no changesets available in the repository"), - @ResponseCode(code = 500, condition = "internal server error") - }) + @Path("{branch}/changesets/") @Produces(VndMediaType.CHANGESET_COLLECTION) - @TypeHint(CollectionDto.class) + @Operation(summary = "Collection of changesets", description = "Returns a collection of changesets for specific branch.", tags = "Repository") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = VndMediaType.CHANGESET_COLLECTION, + schema = @Schema(implementation = CollectionDto.class) + ) + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the changeset") + @ApiResponse(responseCode = "404", description = "not found, no changesets available in the repository") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + ) + ) public Response history(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("branch") String branchName, @@ -143,14 +168,18 @@ public class BranchRootResource { @POST @Path("") @Consumes(VndMediaType.BRANCH_REQUEST) - @StatusCodes({ - @ResponseCode(code = 201, condition = "create success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"push\" privilege"), - @ResponseCode(code = 409, condition = "conflict, a user with this name already exists"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) + @Operation(summary = "Create branch", description = "Creates a new branch.", tags = "Repository") + @ApiResponse(responseCode = "201", description = "create success") + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"push\" privilege") + @ApiResponse(responseCode = "409", description = "conflict, a branch with this name already exists") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) @ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created branch")) public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name, @@ -195,15 +224,33 @@ public class BranchRootResource { @GET @Path("") @Produces(VndMediaType.BRANCH_COLLECTION) - @TypeHint(CollectionDto.class) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 400, condition = "branches not supported for given repository"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"read repository\" privilege"), - @ResponseCode(code = 404, condition = "not found, no repository found for the given namespace and name"), - @ResponseCode(code = 500, condition = "internal server error") - }) + @Operation(summary = "List of branches", description = "Returns all branches for a repository.", tags = "Repository") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = VndMediaType.BRANCH_COLLECTION, + schema = @Schema(implementation = CollectionDto.class) + ) + ) + @ApiResponse(responseCode = "400", description = "branches not supported for given repository") + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"read repository\" privilege") + @ApiResponse( + responseCode = "404", + description = "not found, no repository found for the given namespace and name", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + ) + ) + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) throws IOException { try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { Branches branches = repositoryService.getBranchesCommand().getBranches(); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetRootResource.java index 3921839b76..1b1bc7b0a1 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetRootResource.java @@ -1,11 +1,9 @@ package sonia.scm.api.v2.resources; -import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; import sonia.scm.PageResult; import sonia.scm.repository.Changeset; @@ -45,58 +43,10 @@ public class ChangesetRootResource { this.changesetToChangesetDtoMapper = changesetToChangesetDtoMapper; } - @GET - @Path("{id}") - @Produces(VndMediaType.CHANGESET) - @Operation(summary = "Collection of changesets", description = "Returns a collection of changesets.", tags = "Repository") - @ApiResponse( - responseCode = "200", - description = "success", - content = @Content( - mediaType = VndMediaType.CHANGESET, - schema = @Schema(implementation = ChangesetDto.class) - ) - ) - @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") - @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the changeset") - @ApiResponse( - responseCode = "404", - description = "not found, no changeset with the specified id is available in the repository", - content = @Content( - mediaType = VndMediaType.ERROR_TYPE, - schema = @Schema(implementation = ErrorDto.class) - )) - @ApiResponse( - responseCode = "500", - description = "internal server error", - content = @Content( - mediaType = VndMediaType.ERROR_TYPE, - schema = @Schema(implementation = ErrorDto.class) - )) - public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("id") String id) throws IOException { - try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { - Repository repository = repositoryService.getRepository(); - RepositoryPermissions.read(repository).check(); - ChangesetPagingResult changesets = repositoryService.getLogCommand() - .setStartChangeset(id) - .setEndChangeset(id) - .getChangesets(); - if (changesets != null && changesets.getChangesets() != null && !changesets.getChangesets().isEmpty()) { - Optional changeset = changesets.getChangesets().stream().filter(ch -> ch.getId().equals(id)).findFirst(); - if (!changeset.isPresent()) { - return Response.status(Response.Status.NOT_FOUND).build(); - } - return Response.ok(changesetToChangesetDtoMapper.map(changeset.get(), repository)).build(); - } else { - return Response.status(Response.Status.NOT_FOUND).build(); - } - } - } - @GET @Path("") @Produces(VndMediaType.CHANGESET_COLLECTION) - @Operation(summary = "Specific changeset", description = "Returns a specific changeset.", tags = "Repository") + @Operation(summary = "Collection of changesets", description = "Returns a collection of changesets.", tags = "Repository") @ApiResponse( responseCode = "200", description = "success", @@ -143,4 +93,52 @@ public class ChangesetRootResource { } } } + + @GET + @Path("{id}") + @Produces(VndMediaType.CHANGESET) + @Operation(summary = "Specific changeset", description = "Returns a specific changeset.", tags = "Repository") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = VndMediaType.CHANGESET, + schema = @Schema(implementation = ChangesetDto.class) + ) + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the changeset") + @ApiResponse( + responseCode = "404", + description = "not found, no changeset with the specified id is available in the repository", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("id") String id) throws IOException { + try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { + Repository repository = repositoryService.getRepository(); + RepositoryPermissions.read(repository).check(); + ChangesetPagingResult changesets = repositoryService.getLogCommand() + .setStartChangeset(id) + .setEndChangeset(id) + .getChangesets(); + if (changesets != null && changesets.getChangesets() != null && !changesets.getChangesets().isEmpty()) { + Optional changeset = changesets.getChangesets().stream().filter(ch -> ch.getId().equals(id)).findFirst(); + if (!changeset.isPresent()) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + return Response.ok(changesetToChangesetDtoMapper.map(changeset.get(), repository)).build(); + } else { + return Response.status(Response.Status.NOT_FOUND).build(); + } + } + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java index 510657c797..0e54d6855d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java @@ -78,7 +78,7 @@ public class GroupResource { */ @DELETE @Path("") - @Operation(summary = "Delete group", description = "Deletes a group.", tags = "Group") + @Operation(summary = "Delete group", description = "Deletes the group with the given id.", tags = "Group") @ApiResponse(responseCode = "204", description = "delete success or nothing to delete") @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the group") diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IncomingRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IncomingRootResource.java index 1062e60c90..b07f52ecd8 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IncomingRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IncomingRootResource.java @@ -1,9 +1,10 @@ package sonia.scm.api.v2.resources; import com.google.inject.Inject; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import sonia.scm.PageResult; import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; @@ -81,17 +82,34 @@ public class IncomingRootResource { * @return * @throws Exception */ - @Path("{source}/{target}/changesets") @GET - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the changeset"), - @ResponseCode(code = 404, condition = "not found, no changesets available in the repository"), - @ResponseCode(code = 500, condition = "internal server error") - }) + @Path("{source}/{target}/changesets") @Produces(VndMediaType.CHANGESET_COLLECTION) - @TypeHint(CollectionDto.class) + @Operation(summary = "Incoming changesets", description = "Get the incoming changesets from source to target", tags = "Repository") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = VndMediaType.CHANGESET_COLLECTION, + schema = @Schema(implementation = CollectionDto.class) + ) + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the changeset") + @ApiResponse( + responseCode = "404", + description = "not found, no changesets available in the repository", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response incomingChangesets(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("source") String source, @@ -117,18 +135,34 @@ public class IncomingRootResource { } } - - @Path("{source}/{target}/diff") @GET - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the changeset"), - @ResponseCode(code = 404, condition = "not found, no changesets available in the repository"), - @ResponseCode(code = 500, condition = "internal server error") - }) + @Path("{source}/{target}/diff") @Produces(VndMediaType.DIFF) - @TypeHint(CollectionDto.class) + @Operation(summary = "Incoming diff", description = "Get the incoming diff from source to target", tags = "Repository") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = VndMediaType.DIFF, + schema = @Schema(implementation = CollectionDto.class) + ) + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the changeset") + @ApiResponse( + responseCode = "404", + description = "not found, no changesets available in the repository", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response incomingDiff(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("source") String source, @@ -155,14 +189,32 @@ public class IncomingRootResource { @GET @Path("{source}/{target}/diff/parsed") @Produces(VndMediaType.DIFF_PARSED) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 400, condition = "Bad Request"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the diff"), - @ResponseCode(code = 404, condition = "not found, source or target branch for the repository not available or repository not found"), - @ResponseCode(code = 500, condition = "internal server error") - }) + @Operation(summary = "Incoming parsed diff", description = "Get the incoming parsed diff from source to target", tags = "Repository") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = VndMediaType.DIFF_PARSED, + schema = @Schema(implementation = CollectionDto.class) + ) + ) + @ApiResponse(responseCode = "400", description = "bad request") + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the diff") + @ApiResponse( + responseCode = "404", + description = "not found, source or target branch for the repository not available or repository not found", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response incomingDiffParsed(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("source") String source, diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ModificationsRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ModificationsRootResource.java index eaf165cda1..50cdc0c617 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ModificationsRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ModificationsRootResource.java @@ -1,8 +1,9 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import sonia.scm.repository.Modifications; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.api.RepositoryService; @@ -33,16 +34,27 @@ public class ModificationsRootResource { * file modifications are for example: Modified, Added or Removed. */ @GET - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the modifications"), - @ResponseCode(code = 404, condition = "not found, no changeset with the specified id is available in the repository"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @Produces(VndMediaType.MODIFICATIONS) - @TypeHint(ModificationsDto.class) @Path("{revision}") + @Produces(VndMediaType.MODIFICATIONS) + @Operation(summary = "File modifications", description = "Get the file modifications related to a revision.", tags = "Repository") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = VndMediaType.MODIFICATIONS, + schema = @Schema(implementation = ModificationsDto.class) + ) + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the modifications") + @ApiResponse( + responseCode = "404", + description = "not found, no changeset with the specified id is available in the repository", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + @ApiResponse(responseCode = "500", description = "internal server error") public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision) throws IOException { try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { Modifications modifications = repositoryService.getModificationsCommand() 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 7b87c612d7..f1d0a107ca 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 @@ -1,6 +1,7 @@ package sonia.scm.api.v2.resources; import de.otto.edison.hal.Links; +import io.swagger.v3.oas.annotations.Operation; import sonia.scm.repository.NamespaceStrategy; import sonia.scm.web.VndMediaType; @@ -42,6 +43,7 @@ public class NamespaceStrategyResource { @GET @Path("") @Produces(VndMediaType.NAMESPACE_STRATEGIES) + @Operation(summary = "List of namespace strategies", description = "Returns all available namespace strategies and the current selected.", tags = "Repository") public NamespaceStrategiesDto get(@Context UriInfo uriInfo) { String currentStrategy = strategyAsString(namespaceStrategyProvider.get()); List availableStrategies = collectStrategyNames(); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java index d27b598646..ed12dab1ee 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java @@ -1,10 +1,11 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.ResponseHeader; import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import org.apache.shiro.SecurityUtils; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryInitializer; @@ -63,13 +64,26 @@ public class RepositoryCollectionResource { @GET @Path("") @Produces(VndMediaType.REPOSITORY_COLLECTION) - @TypeHint(CollectionDto.class) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"repository\" privilege"), - @ResponseCode(code = 500, condition = "internal server error") - }) + @Operation(summary = "List of repositories", description = "Returns all repositories for a given page number with a given page size.", tags = "Repository") + + + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = VndMediaType.REPOSITORY_COLLECTION, + schema = @Schema(implementation = CollectionDto.class) + ) + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"repository\" privilege") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response getAll(@DefaultValue("0") @QueryParam("page") int page, @DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize, @QueryParam("sortBy") String sortBy, @@ -92,14 +106,18 @@ public class RepositoryCollectionResource { @POST @Path("") @Consumes(VndMediaType.REPOSITORY) - @StatusCodes({ - @ResponseCode(code = 201, condition = "create success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"repository\" privilege"), - @ResponseCode(code = 409, condition = "conflict, a repository with this name already exists"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) + @Operation(summary = "Create repository", description = "Creates a new repository.", tags = "Repository") + @ApiResponse(responseCode = "201", description = "create success") + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"repository\" privilege") + @ApiResponse(responseCode = "409", description = "conflict, a repository with this name already exists") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) @ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created repository")) public Response create(@Valid RepositoryDto repository, @QueryParam("initialize") boolean initialize) { AtomicReference reference = new AtomicReference<>(); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResource.java index b87a4911a5..1cebbba3c7 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResource.java @@ -1,9 +1,10 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.ResponseHeader; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import lombok.extern.slf4j.Slf4j; import sonia.scm.AlreadyExistsException; import sonia.scm.NotFoundException; @@ -64,17 +65,30 @@ public class RepositoryPermissionRootResource { * @return a web response with the status code 201 and the url to GET the added permission */ @POST - @StatusCodes({ - @ResponseCode(code = 201, condition = "creates", additionalHeaders = { - @ResponseHeader(name = "Location", description = "uri of the created permission") - }), - @ResponseCode(code = 500, condition = "internal server error"), - @ResponseCode(code = 404, condition = "not found"), - @ResponseCode(code = 409, condition = "conflict") - }) - @TypeHint(TypeHint.NO_CONTENT.class) - @Consumes(VndMediaType.REPOSITORY_PERMISSION) @Path("") + @Consumes(VndMediaType.REPOSITORY_PERMISSION) + @Operation(summary = "Create repository-specific permission", description = "Adds a new permission to the user or group managed by the repository.", tags = {"Repository", "Permissions"}) + @ApiResponse( + responseCode = "201", + description = "creates", + headers = @Header(name = "Location", description = "uri of the created permission") + ) + @ApiResponse( + responseCode = "404", + description = "not found", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + @ApiResponse(responseCode = "409", description = "conflict") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + ) + ) public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid RepositoryPermissionDto permission) { log.info("try to add new permission: {}", permission); Repository repository = load(namespace, name); @@ -95,14 +109,32 @@ public class RepositoryPermissionRootResource { * @throws NotFoundException if the repository does not exists */ @GET - @StatusCodes({ - @ResponseCode(code = 200, condition = "ok"), - @ResponseCode(code = 404, condition = "not found"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @Produces(VndMediaType.REPOSITORY_PERMISSION) - @TypeHint(RepositoryPermissionDto.class) @Path("{permission-name}") + @Produces(VndMediaType.REPOSITORY_PERMISSION) + @Operation(summary = "Get single repository-specific permission", description = "Get the searched permission with permission name related to a repository.", tags = {"Repository", "Permissions"}) + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = VndMediaType.REPOSITORY_PERMISSION, + schema = @Schema(implementation = RepositoryPermissionDto.class) + ) + ) + @ApiResponse( + responseCode = "404", + description = "not found", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + ) + ) public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("permission-name") String permissionName) { Repository repository = load(namespace, name); RepositoryPermissions.permissionRead(repository).check(); @@ -125,14 +157,32 @@ public class RepositoryPermissionRootResource { * @throws NotFoundException if the repository does not exists */ @GET - @StatusCodes({ - @ResponseCode(code = 200, condition = "ok"), - @ResponseCode(code = 404, condition = "not found"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @Produces(VndMediaType.REPOSITORY_PERMISSION) - @TypeHint(RepositoryPermissionDto.class) @Path("") + @Produces(VndMediaType.REPOSITORY_PERMISSION) + @Operation(summary = "List of repository-specific permissions", description = "Get all permissions related to a repository.", tags = {"Repository", "Permissions"}) + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = VndMediaType.REPOSITORY_PERMISSION, + schema = @Schema(implementation = RepositoryPermissionDto.class) + ) + ) + @ApiResponse( + responseCode = "404", + description = "not found", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + ) + ) public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) { Repository repository = load(namespace, name); RepositoryPermissions.permissionRead(repository).check(); @@ -148,14 +198,19 @@ public class RepositoryPermissionRootResource { * @return a web response with the status code 204 */ @PUT - @StatusCodes({ - @ResponseCode(code = 204, condition = "update success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) - @Consumes(VndMediaType.REPOSITORY_PERMISSION) @Path("{permission-name}") + @Consumes(VndMediaType.REPOSITORY_PERMISSION) + @Operation(summary = "Update repository-specific permission", description = "Update a permission to the user or group managed by the repository.", tags = {"Repository", "Permissions"}) + @ApiResponse(responseCode = "204", description = "update success") + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + ) + ) public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("permission-name") String permissionName, @@ -194,14 +249,19 @@ public class RepositoryPermissionRootResource { * @return a web response with the status code 204 */ @DELETE - @StatusCodes({ - @ResponseCode(code = 204, condition = "delete success or nothing to delete"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) @Path("{permission-name}") + @Operation(summary = "Delete repository-specific permission", description = "Delete a permission with the given name.", tags = {"Repository", "Permissions"}) + @ApiResponse(responseCode = "204", description = "delete success or nothing to delete") + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + ) + ) public Response delete(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("permission-name") String permissionName) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index 8eb1c204a7..ed29ac9b52 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -1,8 +1,5 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -91,7 +88,7 @@ public class RepositoryResource { @GET @Path("") @Produces(VndMediaType.REPOSITORY) - @Operation(summary = "Returns a single repository", description = "Returns the repository for the given namespace and name.", tags = "Repository") + @Operation(summary = "Get single repository", description = "Returns the repository for the given namespace and name.", tags = "Repository") @ApiResponse( responseCode = "200", description = "success", @@ -139,13 +136,11 @@ public class RepositoryResource { */ @DELETE @Path("") - @StatusCodes({ - @ResponseCode(code = 204, condition = "delete success or nothing to delete"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"repository\" privilege"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) + @Operation(summary = "Delete repository", description = "Deletes the repository with the given namespace and name.", tags = "Repository") + @ApiResponse(responseCode = "204", description = "delete success or nothing to delete") + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"repository\" privilege") + @ApiResponse(responseCode = "500", description = "internal server error") public Response delete(@PathParam("namespace") String namespace, @PathParam("name") String name) { return adapter.delete(loadBy(namespace, name)); } @@ -162,15 +157,19 @@ public class RepositoryResource { @PUT @Path("") @Consumes(VndMediaType.REPOSITORY) - @StatusCodes({ - @ResponseCode(code = 204, condition = "update success"), - @ResponseCode(code = 400, condition = "invalid body, e.g. illegal change of namespace or name"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"repository\" privilege"), - @ResponseCode(code = 404, condition = "not found, no repository with the specified namespace and name available"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) + @Operation(summary = "Update repository", description = "Modifies the repository for the given namespace and name.", tags = "Repository") + @ApiResponse(responseCode = "204", description = "update success") + @ApiResponse(responseCode = "400", description = "invalid body, e.g. illegal change of namespace or name") + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"repository\" privilege") + @ApiResponse( + responseCode = "404", + description = "not found, no repository with the specified namespace and name available", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + @ApiResponse(responseCode = "500", description = "internal server error") public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid RepositoryDto repository) { return adapter.update( loadBy(namespace, name), diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleCollectionResource.java index 99d8672eec..60f39f774f 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleCollectionResource.java @@ -1,10 +1,11 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.ResponseHeader; import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import sonia.scm.repository.RepositoryRole; import sonia.scm.repository.RepositoryRoleManager; import sonia.scm.web.VndMediaType; @@ -51,14 +52,25 @@ public class RepositoryRoleCollectionResource { @GET @Path("") @Produces(VndMediaType.REPOSITORY_ROLE_COLLECTION) - @TypeHint(CollectionDto.class) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 400, condition = "\"sortBy\" field unknown"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current repositoryRole does not have the \"repositoryRole\" privilege"), - @ResponseCode(code = 500, condition = "internal server error") - }) + @Operation(summary = "List of repository roles", description = "Returns all repository roles for a given page number with a given page size.", tags = "Repository role") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = VndMediaType.REPOSITORY_ROLE_COLLECTION, + schema = @Schema(implementation = CollectionDto.class) + ) + ) + @ApiResponse(responseCode = "400", description = "\"sortBy\" field unknown") + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current repositoryRole does not have the \"repositoryRole\" privilege") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response getAll(@DefaultValue("0") @QueryParam("page") int page, @DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize, @QueryParam("sortBy") String sortBy, @@ -79,14 +91,18 @@ public class RepositoryRoleCollectionResource { @POST @Path("") @Consumes(VndMediaType.REPOSITORY_ROLE) - @StatusCodes({ - @ResponseCode(code = 201, condition = "create success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"repositoryRole\" privilege"), - @ResponseCode(code = 409, condition = "conflict, a repository role with this name already exists"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) + @Operation(summary = "Create repository role", description = "Creates a new repository role.", tags = "Repository role") + @ApiResponse(responseCode = "201", description = "create success") + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"repositoryRole\" privilege") + @ApiResponse(responseCode = "409", description = "conflict, a repository role with this name already exists") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) @ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created repositoryRole")) public Response create(@Valid RepositoryRoleDto repositoryRole) { return adapter.create(repositoryRole, () -> dtoToRepositoryRoleMapper.map(repositoryRole), u -> resourceLinks.repositoryRole().self(u.getName())); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleResource.java index 3a85fb5377..7f38eaa0c2 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleResource.java @@ -1,8 +1,9 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import sonia.scm.repository.RepositoryRole; import sonia.scm.repository.RepositoryRoleManager; import sonia.scm.web.VndMediaType; @@ -45,14 +46,31 @@ public class RepositoryRoleResource { @GET @Path("") @Produces(VndMediaType.REPOSITORY_ROLE) - @TypeHint(RepositoryRoleDto.class) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the repository role"), - @ResponseCode(code = 404, condition = "not found, no repository role with the specified name available"), - @ResponseCode(code = 500, condition = "internal server error") - }) + @Operation(summary = "Get single repository role", description = "Returns the repository role for the given name.", tags = "Repository role") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = VndMediaType.REPOSITORY_ROLE, + schema = @Schema(implementation = RepositoryRoleDto.class) + ) + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the repository role") + @ApiResponse( + responseCode = "404", + description = "not found, no repository role with the specified name available", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response get(@PathParam("name") String name) { return adapter.get(name, repositoryRoleToDtoMapper::map); } @@ -66,13 +84,11 @@ public class RepositoryRoleResource { */ @DELETE @Path("") - @StatusCodes({ - @ResponseCode(code = 204, condition = "delete success or nothing to delete"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"repositoryRole\" privilege"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) + @Operation(summary = "Delete repository role", description = "Deletes the repository role with the given name.", tags = "Repository role") + @ApiResponse(responseCode = "204", description = "delete success or nothing to delete") + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"repositoryRole\" privilege") + @ApiResponse(responseCode = "500", description = "internal server error") public Response delete(@PathParam("name") String name) { return adapter.delete(name); } @@ -88,15 +104,19 @@ public class RepositoryRoleResource { @PUT @Path("") @Consumes(VndMediaType.REPOSITORY_ROLE) - @StatusCodes({ - @ResponseCode(code = 204, condition = "update success"), - @ResponseCode(code = 400, condition = "invalid body, e.g. illegal change of repository role name"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"repositoryRole\" privilege"), - @ResponseCode(code = 404, condition = "not found, no repository role with the specified name available"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) + @Operation(summary = "Update repository role", description = "Modifies the repository role for the given name.", tags = "Repository role") + @ApiResponse(responseCode = "204", description = "update success") + @ApiResponse(responseCode = "400", description = "invalid body, e.g. illegal change of repository role name") + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"repositoryRole\" privilege") + @ApiResponse( + responseCode = "404", + description = "not found, no repository role with the specified name available", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + @ApiResponse(responseCode = "500", description = "internal server error") public Response update(@PathParam("name") String name, @Valid RepositoryRoleDto repositoryRole) { return adapter.update(name, existing -> dtoToRepositoryRoleMapper.map(repositoryRole)); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleRootResource.java index 44e3a10fba..8d47963778 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleRootResource.java @@ -1,5 +1,8 @@ package sonia.scm.api.v2.resources; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.tags.Tag; + import javax.inject.Inject; import javax.inject.Provider; import javax.ws.rs.Path; @@ -7,6 +10,9 @@ import javax.ws.rs.Path; /** * RESTful web service resource to manage repository roles. */ +@OpenAPIDefinition(tags = { + @Tag(name = "Repository role", description = "Repository role related endpoints") +}) @Path(RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2) public class RepositoryRoleRootResource { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryTypeCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryTypeCollectionResource.java index 4f9e76a939..8f33381497 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryTypeCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryTypeCollectionResource.java @@ -1,8 +1,10 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; import de.otto.edison.hal.HalRepresentation; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import sonia.scm.repository.RepositoryManager; import sonia.scm.web.VndMediaType; @@ -24,11 +26,25 @@ public class RepositoryTypeCollectionResource { @GET @Path("") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 500, condition = "internal server error") - }) @Produces(VndMediaType.REPOSITORY_TYPE_COLLECTION) + @Operation(summary = "List of repository types", description = "Returns all repository types.", tags = "Repository") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = VndMediaType.REPOSITORY_TYPE_COLLECTION, + schema = @Schema(implementation = HalRepresentation.class) + ) + ) + @ApiResponse(responseCode = "400", description = "\"sortBy\" field unknown") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + ) + ) public HalRepresentation getAll() { return mapper.map(repositoryManager.getConfiguredTypes()); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryTypeResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryTypeResource.java index 3a47fdf6c6..fa1c5b66d3 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryTypeResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryTypeResource.java @@ -1,8 +1,9 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryType; import sonia.scm.web.VndMediaType; @@ -35,12 +36,24 @@ public class RepositoryTypeResource { @GET @Path("") @Produces(VndMediaType.REPOSITORY_TYPE) - @TypeHint(RepositoryTypeDto.class) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 404, condition = "not found, no repository type with the specified name available"), - @ResponseCode(code = 500, condition = "internal server error") - }) + @Operation(summary = "Get single repository type", description = "Returns the specified repository type for the given name.", tags = "Repository") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = VndMediaType.REPOSITORY_TYPE, + schema = @Schema(implementation = RepositoryTypeDto.class) + ) + ) + @ApiResponse(responseCode = "404", description = "not found, no repository type with the specified name available") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + ) + ) public Response get(@PathParam("name") String name) { for (RepositoryType type : repositoryManager.getConfiguredTypes()) { if (name.equalsIgnoreCase(type.getName())) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryVerbResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryVerbResource.java index 4d4de067e5..0fde9d2911 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryVerbResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryVerbResource.java @@ -1,8 +1,10 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; import de.otto.edison.hal.Links; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import sonia.scm.security.RepositoryPermissionProvider; import sonia.scm.web.VndMediaType; @@ -30,11 +32,23 @@ public class RepositoryVerbResource { @GET @Path("") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 500, condition = "internal server error") - }) @Produces(VndMediaType.REPOSITORY_VERB_COLLECTION) + @Operation(summary = "List of repository verbs", description = "Returns all repository-specific permissions.", hidden = true) + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = VndMediaType.REPOSITORY_VERB_COLLECTION, + schema = @Schema(implementation = RepositoryVerbsDto.class) + ) + ) + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public RepositoryVerbsDto getAll() { return new RepositoryVerbsDto( Links.linkingTo().self(resourceLinks.repositoryVerbs().self()).build(), diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java index 758afd7660..dfd7dc8b5b 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java @@ -1,5 +1,6 @@ package sonia.scm.api.v2.resources; +import io.swagger.v3.oas.annotations.Operation; import sonia.scm.repository.BrowserResult; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.api.BrowseCommandBuilder; @@ -32,22 +33,25 @@ public class SourceRootResource { } @GET - @Produces(VndMediaType.SOURCE) @Path("") + @Produces(VndMediaType.SOURCE) + @Operation(summary = "List of sources", description = "Returns all sources for repository head.", tags = "Repository") public Response getAllWithoutRevision(@PathParam("namespace") String namespace, @PathParam("name") String name) throws IOException { return getSource(namespace, name, "/", null); } @GET - @Produces(VndMediaType.SOURCE) @Path("{revision}") + @Produces(VndMediaType.SOURCE) + @Operation(summary = "List of sources by revision", description = "Returns all sources for the given revision.", tags = "Repository") public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision) throws IOException { return getSource(namespace, name, "/", revision); } @GET - @Produces(VndMediaType.SOURCE) @Path("{revision}/{path: .*}") + @Produces(VndMediaType.SOURCE) + @Operation(summary = "List of sources by revision in path", description = "Returns all sources for the given revision in a specific path.", tags = "Repository") public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path) throws IOException { return getSource(namespace, name, path, revision); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagRootResource.java index 7acd59e3e1..9294128fee 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagRootResource.java @@ -1,8 +1,9 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import sonia.scm.NotFoundException; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; @@ -39,17 +40,27 @@ public class TagRootResource { this.tagToTagDtoMapper = tagToTagDtoMapper; } - @GET @Path("") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the tags"), - @ResponseCode(code = 500, condition = "internal server error") - }) @Produces(VndMediaType.TAG_COLLECTION) - @TypeHint(CollectionDto.class) + @Operation(summary = "List of tags", description = "Returns the tags for the given namespace and name.", tags = "Repository") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = VndMediaType.TAG_COLLECTION, + schema = @Schema(implementation = CollectionDto.class) + ) + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the tags") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) throws IOException { try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { Tags tags = getTags(repositoryService); @@ -65,16 +76,33 @@ public class TagRootResource { @GET - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the tags"), - @ResponseCode(code = 404, condition = "not found, no tag available in the repository"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @Produces(VndMediaType.TAG) - @TypeHint(TagDto.class) @Path("{tagName}") + @Produces(VndMediaType.TAG) + @Operation(summary = "Get tag", description = "Returns the tag for the given name.", tags = "Repository") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = VndMediaType.TAG, + schema = @Schema(implementation = TagDto.class) + ) + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the tags") + @ApiResponse( + responseCode = "404", + description = "not found, no tag with the specified name available in the repository", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("tagName") String tagName) throws IOException { NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name); try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginResource.java index 1c779653a0..4efb0c6446 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginResource.java @@ -1,8 +1,9 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import sonia.scm.plugin.PluginLoader; import sonia.scm.plugin.InstalledPlugin; import sonia.scm.security.AllowAnonymousAccess; @@ -39,12 +40,23 @@ public class UIPluginResource { */ @GET @Path("") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(CollectionDto.class) @Produces(VndMediaType.UI_PLUGIN_COLLECTION) + @Operation(summary = "Collection of ui plugin bundles", description = "Returns a collection of installed plugins and their ui bundles.", hidden = true) + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = VndMediaType.UI_PLUGIN_COLLECTION, + schema = @Schema(implementation = CollectionDto.class) + ) + ) + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response getInstalledPlugins() { List plugins = pluginLoader.getInstalledPlugins() .stream() @@ -63,13 +75,30 @@ public class UIPluginResource { */ @GET @Path("{id}") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 404, condition = "not found"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(UIPluginDto.class) @Produces(VndMediaType.UI_PLUGIN) + @Operation(summary = "Get single ui plugin bundle", description = "Returns the installed plugin with the given id.", hidden = true) + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = VndMediaType.UI_PLUGIN, + schema = @Schema(implementation = UIPluginDto.class) + ) + ) + @ApiResponse( + responseCode = "404", + description = "not found", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response getInstalledPlugin(@PathParam("id") String id) { Optional uiPluginDto = pluginLoader.getInstalledPlugins() .stream() diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java index 016eb27430..e4dfe6e633 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java @@ -54,7 +54,7 @@ public class UserResource { @GET @Path("") @Produces(VndMediaType.USER) - @Operation(summary = "Returns a single user", description = "Returns the user for the given id.", tags = "User") + @Operation(summary = "Get single user", description = "Returns the user for the given id.", tags = "User") @ApiResponse( responseCode = "200", description = "success", @@ -92,7 +92,7 @@ public class UserResource { */ @DELETE @Path("") - @Operation(summary = "Deletes a user", description = "Deletes the user for the given id.", tags = "User") + @Operation(summary = "Delete user", description = "Deletes the user with the given id.", tags = "User") @ApiResponse(responseCode = "204", description = "delete success or nothing to delete") @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"user\" privilege") @@ -113,7 +113,7 @@ public class UserResource { @PUT @Path("") @Consumes(VndMediaType.USER) - @Operation(summary = "Modifies a user", description = "Modifies the user for the given id.", tags = "User") + @Operation(summary = "Update user", description = "Modifies the user for the given id.", tags = "User") @ApiResponse(responseCode = "204", description = "update success") @ApiResponse(responseCode = "400", description = "invalid body, e.g. illegal change of id/user name") @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") From 56202e52666503271667fb0e62d6b9b80d63fcff Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Wed, 19 Feb 2020 17:00:33 +0100 Subject: [PATCH 29/46] Remove unused imports, adding missing dots in descriptions, remove last TypeHint annotation --- .../scm/api/v2/resources/GitRepositoryConfigResource.java | 2 -- .../sonia/scm/api/v2/resources/IncomingRootResource.java | 6 +++--- .../main/java/sonia/scm/api/v2/resources/IndexResource.java | 1 - .../sonia/scm/api/v2/resources/InstalledPluginResource.java | 2 -- .../scm/api/v2/resources/RepositoryCollectionResource.java | 2 -- 5 files changed, 3 insertions(+), 10 deletions(-) diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigResource.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigResource.java index 88a9c5d669..af7eb23c63 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigResource.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigResource.java @@ -1,7 +1,5 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IncomingRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IncomingRootResource.java index b07f52ecd8..2d1cc1cf99 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IncomingRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IncomingRootResource.java @@ -85,7 +85,7 @@ public class IncomingRootResource { @GET @Path("{source}/{target}/changesets") @Produces(VndMediaType.CHANGESET_COLLECTION) - @Operation(summary = "Incoming changesets", description = "Get the incoming changesets from source to target", tags = "Repository") + @Operation(summary = "Incoming changesets", description = "Get the incoming changesets from source to target.", tags = "Repository") @ApiResponse( responseCode = "200", description = "success", @@ -138,7 +138,7 @@ public class IncomingRootResource { @GET @Path("{source}/{target}/diff") @Produces(VndMediaType.DIFF) - @Operation(summary = "Incoming diff", description = "Get the incoming diff from source to target", tags = "Repository") + @Operation(summary = "Incoming diff", description = "Get the incoming diff from source to target.", tags = "Repository") @ApiResponse( responseCode = "200", description = "success", @@ -189,7 +189,7 @@ public class IncomingRootResource { @GET @Path("{source}/{target}/diff/parsed") @Produces(VndMediaType.DIFF_PARSED) - @Operation(summary = "Incoming parsed diff", description = "Get the incoming parsed diff from source to target", tags = "Repository") + @Operation(summary = "Incoming parsed diff", description = "Get the incoming parsed diff from source to target.", tags = "Repository") @ApiResponse( responseCode = "200", description = "success", diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexResource.java index cf482edd11..15e0d293af 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexResource.java @@ -57,7 +57,6 @@ public class IndexResource { schema = @Schema(implementation = ErrorDto.class) ) ) - @TypeHint(IndexDto.class) public IndexDto getIndex() { return indexDtoGenerator.generate(); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InstalledPluginResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InstalledPluginResource.java index c74f88a185..d5c891c2df 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InstalledPluginResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InstalledPluginResource.java @@ -1,7 +1,5 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; import de.otto.edison.hal.HalRepresentation; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java index ed12dab1ee..f4eb28770f 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java @@ -65,8 +65,6 @@ public class RepositoryCollectionResource { @Path("") @Produces(VndMediaType.REPOSITORY_COLLECTION) @Operation(summary = "List of repositories", description = "Returns all repositories for a given page number with a given page size.", tags = "Repository") - - @ApiResponse( responseCode = "200", description = "success", From 051e6f946f9355b55f4002a3d57a8e7b0f181e21 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 20 Feb 2020 11:31:21 +0100 Subject: [PATCH 30/46] improved footer layout --- .../{avatar.png => hitchhiker.png} | Bin .../src/__resources__/marvin.jpg | Bin 0 -> 16018 bytes .../src/layout/Footer.stories.tsx | 16 ++++++---- scm-ui/ui-components/src/layout/Footer.tsx | 28 ++++++++-------- .../src/layout/FooterSection.tsx | 6 +--- .../src/navigation/ExternalLink.tsx | 30 ++++++++++++++++++ scm-ui/ui-styles/src/scm.scss | 6 +++- .../ui-webapp/public/locales/de/commons.json | 13 ++++++++ .../ui-webapp/public/locales/en/commons.json | 13 ++++++++ 9 files changed, 85 insertions(+), 27 deletions(-) rename scm-ui/ui-components/src/__resources__/{avatar.png => hitchhiker.png} (100%) create mode 100644 scm-ui/ui-components/src/__resources__/marvin.jpg create mode 100644 scm-ui/ui-components/src/navigation/ExternalLink.tsx diff --git a/scm-ui/ui-components/src/__resources__/avatar.png b/scm-ui/ui-components/src/__resources__/hitchhiker.png similarity index 100% rename from scm-ui/ui-components/src/__resources__/avatar.png rename to scm-ui/ui-components/src/__resources__/hitchhiker.png diff --git a/scm-ui/ui-components/src/__resources__/marvin.jpg b/scm-ui/ui-components/src/__resources__/marvin.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a98f6b09cd2dbc2905a2db739145fb84693338b0 GIT binary patch literal 16018 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 rhX { +const bindAvatar = (binder: Binder, avatar: string) => { binder.bind(EXTENSION_POINT, () => { return avatar; }); }; const bindLinks = (binder: Binder) => { - binder.bind("footer.links", () => REST API); - binder.bind("footer.links", () => CLI); + binder.bind("footer.information", () => ); + binder.bind("footer.information", () => ); + binder.bind("footer.support", () => ); binder.bind("profile.setting", () => ); }; @@ -42,7 +46,7 @@ storiesOf("Layout|Footer", module) }) .add("With Avatar", () => { const binder = new Binder("avatar-story"); - bindAvatar(binder); + bindAvatar(binder, hitchhiker); return withBinder(binder); }) .add("With Plugin Links", () => { @@ -52,7 +56,7 @@ storiesOf("Layout|Footer", module) }) .add("Full", () => { const binder = new Binder("link-story"); - bindAvatar(binder); + bindAvatar(binder, marvin); bindLinks(binder); return withBinder(binder); }); diff --git a/scm-ui/ui-components/src/layout/Footer.tsx b/scm-ui/ui-components/src/layout/Footer.tsx index 35916a7f52..515d3410b5 100644 --- a/scm-ui/ui-components/src/layout/Footer.tsx +++ b/scm-ui/ui-components/src/layout/Footer.tsx @@ -6,6 +6,8 @@ import NavLink from "../navigation/NavLink"; import FooterSection from "./FooterSection"; import styled from "styled-components"; import { EXTENSION_POINT } from "../avatar/Avatar"; +import ExternalLink from "../navigation/ExternalLink"; +import { useTranslation } from "react-i18next"; type Props = { me?: Me; @@ -50,10 +52,12 @@ const TitleWithAvatar: FC = ({ me }) => ( ); const Footer: FC = ({ me, version, links }) => { + const [t] = useTranslation("commons"); const binder = useBinder(); if (!me) { return null; } + const extensionProps = { me, url: "/me", links }; let meSectionTile; if (binder.hasExtension(EXTENSION_POINT)) { @@ -67,24 +71,18 @@ const Footer: FC = ({ me, version, links }) => {

- - + + - }> - - SCM-Manager {version} - - + }> + + - }> - - Learn more - - - Powered by Cloudogu - - + }> + + +
diff --git a/scm-ui/ui-components/src/layout/FooterSection.tsx b/scm-ui/ui-components/src/layout/FooterSection.tsx index 8f94499ff7..57476d31fe 100644 --- a/scm-ui/ui-components/src/layout/FooterSection.tsx +++ b/scm-ui/ui-components/src/layout/FooterSection.tsx @@ -18,11 +18,7 @@ const FooterSection: FC = ({ title, children }) => { return (
{title} - - {React.Children.map(children, (child, index) => ( -
  • {child}
  • - ))} -
    + {children}
    ); }; diff --git a/scm-ui/ui-components/src/navigation/ExternalLink.tsx b/scm-ui/ui-components/src/navigation/ExternalLink.tsx new file mode 100644 index 0000000000..de9e46ce2c --- /dev/null +++ b/scm-ui/ui-components/src/navigation/ExternalLink.tsx @@ -0,0 +1,30 @@ +import React, { FC } from "react"; +import classNames from "classnames"; + +type Props = { + to: string; + icon?: string; + label: string; +}; + +const ExternalLink: FC = ({ to, icon, label }) => { + let showIcon; + if (icon) { + showIcon = ( + <> + {" "} + + ); + } + + return ( +
  • + + {showIcon} + {label} + +
  • + ); +}; + +export default ExternalLink; diff --git a/scm-ui/ui-styles/src/scm.scss b/scm-ui/ui-styles/src/scm.scss index e4e489f880..3adb51da90 100644 --- a/scm-ui/ui-styles/src/scm.scss +++ b/scm-ui/ui-styles/src/scm.scss @@ -69,8 +69,12 @@ hr.header-with-actions { footer.footer { //height: 100px; - background-color: whitesmoke; + background-color: $white-ter; padding: inherit; + + a { + color: darken($blue, 15%); + } } // 6. Import the rest of Bulma diff --git a/scm-ui/ui-webapp/public/locales/de/commons.json b/scm-ui/ui-webapp/public/locales/de/commons.json index 8c575cc4e9..310c14ead3 100644 --- a/scm-ui/ui-webapp/public/locales/de/commons.json +++ b/scm-ui/ui-webapp/public/locales/de/commons.json @@ -86,5 +86,18 @@ "passwordConfirmFailed": "Passwörter müssen identisch sein!", "submit": "Speichern", "changedSuccessfully": "Passwort erfolgreich geändert!" + }, + "footer": { + "user": { + "profile": "Profil" + }, + "information": { + "title": "Information" + }, + "support": { + "title": "Support", + "community": "Community", + "enterprise": "Enterprise" + } } } diff --git a/scm-ui/ui-webapp/public/locales/en/commons.json b/scm-ui/ui-webapp/public/locales/en/commons.json index f7bab46d22..f57a944c65 100644 --- a/scm-ui/ui-webapp/public/locales/en/commons.json +++ b/scm-ui/ui-webapp/public/locales/en/commons.json @@ -87,5 +87,18 @@ "passwordConfirmFailed": "Passwords have to be identical", "submit": "Submit", "changedSuccessfully": "Password changed successfully" + }, + "footer": { + "user": { + "profile": "Profile" + }, + "information": { + "title": "Information" + }, + "support": { + "title": "Support", + "community": "Community", + "enterprise": "Enterprise" + } } } From c7bac50ff27505f1a5b407f8448237f9dce12820 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 20 Feb 2020 15:19:34 +0100 Subject: [PATCH 31/46] fixed authentication --- .../scm/api/v2/resources/AuthenticationResource.java | 9 ++++----- .../java/sonia/scm/api/v2/resources/IndexResource.java | 1 - 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java index 1c55b54fb1..e0c237aef6 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java @@ -43,16 +43,15 @@ import java.util.Optional; @SecurityScheme( name = "Basic Authentication", description = "HTTP Basic authentication with username and password", - scheme = "Basic", + scheme = "basic", type = SecuritySchemeType.HTTP ), @SecurityScheme( name = "Bearer Token Authentication", - in = SecuritySchemeIn.HEADER, - paramName = "Authorization", - scheme = "Bearer", + description = "Authentication with a jwt bearer token", + scheme = "bearer", bearerFormat = "JWT", - type = SecuritySchemeType.APIKEY + type = SecuritySchemeType.HTTP ) }) @OpenAPIDefinition(tags = { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexResource.java index 15e0d293af..8995e7f979 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexResource.java @@ -1,6 +1,5 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.TypeHint; import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; From 42841512c579880efd8a366318a8280a7e5cc004 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 20 Feb 2020 16:17:06 +0100 Subject: [PATCH 32/46] added example to AuthenticationResource --- .../v2/resources/AuthenticationResource.java | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java index e0c237aef6..8e129570fd 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java @@ -3,10 +3,11 @@ package sonia.scm.api.v2.resources; import com.google.inject.Inject; import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn; import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.security.SecurityScheme; import io.swagger.v3.oas.annotations.security.SecuritySchemes; @@ -79,9 +80,16 @@ public class AuthenticationResource { @POST @Path("access_token") + @Produces(MediaType.TEXT_PLAIN) @Consumes(MediaType.APPLICATION_FORM_URLENCODED) - @Operation(summary = "Login via Form", description = "Form-based authentication.", tags = "Authentication") + @Operation( + summary = "Login via Form", + description = "Form-based authentication.", + tags = "Authentication", + hidden = true + ) @ApiResponse(responseCode = "200", description = "success") + @ApiResponse(responseCode = "204", description = "success without content") @ApiResponse(responseCode = "400", description = "bad request, required parameter is missing") @ApiResponse(responseCode = "401", description = "unauthorized, the specified username or password is wrong") @ApiResponse( @@ -102,9 +110,26 @@ public class AuthenticationResource { @POST @Path("access_token") + @Produces(MediaType.TEXT_PLAIN) @Consumes(MediaType.APPLICATION_JSON) - @Operation(summary = "Login via JSON", description = "JSON-based authentication.", tags = "Authentication") + @Operation( + summary = "Login via JSON", + description = "JSON-based authentication.", + tags = "Authentication", + requestBody = @RequestBody( + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = AuthenticationRequestDto.class), + examples = @ExampleObject( + name = "Simple login", + value = "{\n \"username\":\"scmadmin\",\n \"password\":\"scmadmin\",\n \"cookie\":false,\n \"grant_type\":\"password\"\n}", + summary = "Authenticate with username and password" + ) + ) + ) + ) @ApiResponse(responseCode = "200", description = "success") + @ApiResponse(responseCode = "204", description = "success without content") @ApiResponse(responseCode = "400", description = "bad request, required parameter is missing") @ApiResponse(responseCode = "401", description = "unauthorized, the specified username or password is wrong") @ApiResponse( From 39846314cb9ba3f1e6507c227dbd3e0c27a3bc06 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 20 Feb 2020 16:23:12 +0100 Subject: [PATCH 33/46] update storyshots --- .../src/__snapshots__/storyshots.test.ts.snap | 722 ++++++++++++------ 1 file changed, 482 insertions(+), 240 deletions(-) diff --git a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap index 17a73282c2..790c2cdc94 100644 --- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -484,7 +484,7 @@ exports[`Storyshots DateFromNow Default 1`] = ` exports[`Storyshots Diff Binaries 1`] = `
    `; @@ -32446,79 +32501,145 @@ exports[`Storyshots Layout|Footer Full 1`] = ` `; @@ -32526,65 +32647,112 @@ exports[`Storyshots Layout|Footer With Avatar 1`] = ` `; @@ -32592,66 +32760,140 @@ exports[`Storyshots Layout|Footer With Plugin Links 1`] = ` `; @@ -34509,7 +34751,7 @@ PORT_NUMBER = exports[`Storyshots Table|Table Default 1`] = ` @@ -34527,7 +34769,7 @@ exports[`Storyshots Table|Table Default 1`] = ` > Last Name @@ -34612,7 +34854,7 @@ exports[`Storyshots Table|Table TextColumn 1`] = ` > Id From 7025fbbc2f0675863522b35ab6a77af66214c7ac Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Fri, 21 Feb 2020 12:34:59 +0000 Subject: [PATCH 34/46] Close branch feature/update_node_and_yarn From 6d9256ed9905b841364a3a1b6e29680890fb9929 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Mon, 24 Feb 2020 08:55:17 +0100 Subject: [PATCH 35/46] Add content to 404 response --- .../v2/resources/AvailablePluginResource.java | 8 ++++++- .../api/v2/resources/BranchRootResource.java | 18 ++++++++++++--- .../api/v2/resources/DiffRootResource.java | 16 ++++++++++++-- .../v2/resources/GroupPermissionResource.java | 16 ++++++++++++-- .../scm/api/v2/resources/GroupResource.java | 16 ++++++++++++-- .../v2/resources/InstalledPluginResource.java | 8 ++++++- .../v2/resources/RepositoryTypeResource.java | 8 ++++++- .../v2/resources/UserPermissionResource.java | 22 ++++++++++++------- 8 files changed, 92 insertions(+), 20 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AvailablePluginResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AvailablePluginResource.java index bbd9444f01..7528b43eb2 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AvailablePluginResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AvailablePluginResource.java @@ -104,7 +104,13 @@ public class AvailablePluginResource { ) @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:read\" privilege") - @ApiResponse(responseCode = "404", description = "not found") + @ApiResponse( + responseCode = "404", + description = "not found, no plugin with the specified name found", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) @ApiResponse( responseCode = "500", description = "internal server error", diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java index 1a41508e51..b18be9207e 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java @@ -82,7 +82,13 @@ public class BranchRootResource { @ApiResponse(responseCode = "400", description = "branches not supported for given repository") @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the branch") - @ApiResponse(responseCode = "404", description = "not found, no branch with the specified name for the repository available or repository not found") + @ApiResponse( + responseCode = "404", + description = "not found, no branch with the specified name for the repository available or repository found", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) @ApiResponse( responseCode = "500", description = "internal server error", @@ -122,7 +128,13 @@ public class BranchRootResource { ) @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the changeset") - @ApiResponse(responseCode = "404", description = "not found, no changesets available in the repository") + @ApiResponse( + responseCode = "404", + description = "not found, no changesets available in the repository", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) @ApiResponse( responseCode = "500", description = "internal server error", @@ -137,7 +149,7 @@ public class BranchRootResource { @DefaultValue("0") @QueryParam("page") int page, @DefaultValue("10") @QueryParam("pageSize") int pageSize) throws IOException { try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { - if (!branchExists(branchName, repositoryService)){ + if (!branchExists(branchName, repositoryService)) { throw notFound(entity(Branch.class, branchName).in(Repository.class, namespace + "/" + name)); } Repository repository = repositoryService.getRepository(); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffRootResource.java index 6124cab10b..a41bdb01a3 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffRootResource.java @@ -59,7 +59,13 @@ public class DiffRootResource { @ApiResponse(responseCode = "400", description = "bad request") @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the diff") - @ApiResponse(responseCode = "404", description = "not found, no revision with the specified param for the repository available or repository not found") + @ApiResponse( + responseCode = "404", + description = "not found, no revision with the specified param for the repository available or repository found", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) @ApiResponse( responseCode = "500", description = "internal server error", @@ -97,7 +103,13 @@ public class DiffRootResource { @ApiResponse(responseCode = "400", description = "bad request") @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the diff") - @ApiResponse(responseCode = "404", description = "not found, no revision with the specified param for the repository available or repository not found") + @ApiResponse( + responseCode = "404", + description = "not found, no revision with the specified param for the repository available or repository not found", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) @ApiResponse( responseCode = "500", description = "internal server error", diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java index b00719abbe..8e42ef43e3 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java @@ -50,7 +50,13 @@ public class GroupPermissionResource { )) @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the group") - @ApiResponse(responseCode = "404", description = "not found, no group with the specified id/name available") + @ApiResponse( + responseCode = "404", + description = "not found, no group with the specified id/name available", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) @ApiResponse( responseCode = "500", description = "internal server error", @@ -79,7 +85,13 @@ public class GroupPermissionResource { @ApiResponse(responseCode = "400", description = "invalid body") @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current group does not have the correct privilege") - @ApiResponse(responseCode = "404", description = "not found, no group with the specified id/name available") + @ApiResponse( + responseCode = "404", + description = "not found, no group with the specified id/name available", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) @ApiResponse( responseCode = "500", description = "internal server error", diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java index 0e54d6855d..2a209408db 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java @@ -56,7 +56,13 @@ public class GroupResource { ) @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the group") - @ApiResponse(responseCode = "404", description = "not found, no group with the specified id/name available") + @ApiResponse( + responseCode = "404", + description = "not found, no group with the specified id/name available", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) @ApiResponse( responseCode = "500", description = "internal server error", @@ -110,7 +116,13 @@ public class GroupResource { @ApiResponse(responseCode = "400", description = "invalid body, e.g. illegal change of id/group name") @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"group\" privilege") - @ApiResponse(responseCode = "404", description = "not found, no group with the specified id/name available") + @ApiResponse( + responseCode = "404", + description = "not found, no group with the specified id/name available", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) @ApiResponse( responseCode = "500", description = "internal server error", diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InstalledPluginResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InstalledPluginResource.java index d5c891c2df..a442c70f7e 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InstalledPluginResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InstalledPluginResource.java @@ -126,7 +126,13 @@ public class InstalledPluginResource { ) @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:read\" privilege") - @ApiResponse(responseCode = "404", description = "not found, plugin by given id could not be found") + @ApiResponse( + responseCode = "404", + description = "not found, plugin by given id could not be found", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) @ApiResponse( responseCode = "500", description = "internal server error", diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryTypeResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryTypeResource.java index fa1c5b66d3..b04ce46d98 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryTypeResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryTypeResource.java @@ -45,7 +45,13 @@ public class RepositoryTypeResource { schema = @Schema(implementation = RepositoryTypeDto.class) ) ) - @ApiResponse(responseCode = "404", description = "not found, no repository type with the specified name available") + @ApiResponse( + responseCode = "404", + description = "not found, no repository type with the specified name available", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) @ApiResponse( responseCode = "500", description = "internal server error", diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java index bf8ff4b3c1..5611b32aeb 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java @@ -87,15 +87,21 @@ public class UserPermissionResource { @ApiResponse(responseCode = "400", description = "invalid body") @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the correct privilege") - @ApiResponse(responseCode = "404", description = "not found, no user with the specified id/name available") @ApiResponse( - responseCode = "500", - description = "internal server error", - content = @Content( - mediaType = VndMediaType.ERROR_TYPE, - schema = @Schema(implementation = ErrorDto.class) - ) - ) + responseCode = "404", + description = "not found, no user with the specified id/name available", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + ) + ) public Response overwritePermissions(@PathParam("id") String id, @Valid PermissionListDto newPermissions) { Collection permissionDescriptors = Arrays.stream(newPermissions.getPermissions()) .map(PermissionDescriptor::new) From 7e1e77af2b089e2c1d5053c83c280268bd476af5 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Mon, 24 Feb 2020 13:36:34 +0100 Subject: [PATCH 36/46] remove enunciate since we are using openapi now --- pom.xml | 13 - scm-core/pom.xml | 6 - scm-plugins/pom.xml | 94 ---- .../scm/legacy/LegacyRepositoryService.java | 8 - scm-webapp/pom.xml | 98 ---- scm-webapp/src/main/doc/enunciate.xml | 67 --- .../resources/RepositoryImportResource.java | 427 ++++++------------ .../api/v2/resources/BranchRootResource.java | 13 +- .../v2/resources/GroupCollectionResource.java | 13 +- .../RepositoryCollectionResource.java | 13 +- .../RepositoryRoleCollectionResource.java | 21 +- .../v2/resources/UserCollectionResource.java | 23 +- 12 files changed, 183 insertions(+), 613 deletions(-) delete mode 100644 scm-webapp/src/main/doc/enunciate.xml diff --git a/pom.xml b/pom.xml index 535b9d9af8..999f8f5020 100644 --- a/pom.xml +++ b/pom.xml @@ -184,12 +184,6 @@ true - - com.webcohesion.enunciate - enunciate-core-annotations - ${enunciate.version} - - org.mapstruct mapstruct-jdk8 @@ -453,12 +447,6 @@ 2.3 - - com.webcohesion.enunciate - enunciate-maven-plugin - ${enunciate.version} - - sonia.scm.maven smp-maven-plugin @@ -843,7 +831,6 @@ 2.1.1 4.4.1.Final 1.19.4 - 2.11.1 2.10.0 4.0 2.3.0 diff --git a/scm-core/pom.xml b/scm-core/pom.xml index 563ac0f40f..ec3b884fd6 100644 --- a/scm-core/pom.xml +++ b/scm-core/pom.xml @@ -137,12 +137,6 @@ provided - - - com.webcohesion.enunciate - enunciate-core-annotations - - diff --git a/scm-plugins/pom.xml b/scm-plugins/pom.xml index b74effd8ce..57999aa7d0 100644 --- a/scm-plugins/pom.xml +++ b/scm-plugins/pom.xml @@ -173,101 +173,7 @@ - - - - plugin-doc - - - - - - org.apache.maven.plugins - maven-resources-plugin - - - copy-enunciate-configuration - compile - - copy-resources - - - ${project.build.directory} - - - src/main/doc - true - - **/enunciate.xml - - - - - - - - - - com.webcohesion.enunciate - enunciate-maven-plugin - - - - docs - - compile - - - - ${project.build.directory}/enunciate.xml - ${project.build.directory} - restdocs - - - - com.webcohesion.enunciate - enunciate-top - ${enunciate.version} - - - com.webcohesion.enunciate - enunciate-swagger - - - - - com.webcohesion.enunciate - enunciate-lombok - ${enunciate.version} - - - - - - org.apache.maven.plugins - maven-assembly-plugin - - - src/main/doc/assembly.xml - - - - - package - - single - - - - - - - - - - - diff --git a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyRepositoryService.java b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyRepositoryService.java index d6b923a927..282a802e2e 100644 --- a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyRepositoryService.java +++ b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyRepositoryService.java @@ -1,8 +1,6 @@ package sonia.scm.legacy; import com.google.inject.Inject; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; import sonia.scm.NotFoundException; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryManager; @@ -26,12 +24,6 @@ public class LegacyRepositoryService { @GET @Path("{id}") @Produces(MediaType.APPLICATION_JSON) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"repository:read:global\" privilege"), - @ResponseCode(code = 500, condition = "internal server error") - }) public NamespaceAndNameDto getNameAndNamespaceForRepositoryId(@PathParam("id") String repositoryId) { Repository repo = repositoryManager.get(repositoryId); if (repo == null) { diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index d8068f1bb8..aa52c2dc62 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -916,107 +916,9 @@ - - - - - - - - doc - - - - - - org.apache.maven.plugins - maven-resources-plugin - - - copy-enunciate-configuration - compile - - copy-resources - - - ${project.build.directory} - - - src/main/doc - true - - **/enunciate.xml - - - - - - - - - - com.webcohesion.enunciate - enunciate-maven-plugin - - - - docs - - compile - - - - ${project.build.directory}/enunciate.xml - ${project.build.directory} - restdocs - - - - com.webcohesion.enunciate - enunciate-top - ${enunciate.version} - - - com.webcohesion.enunciate - enunciate-swagger - - - - - com.webcohesion.enunciate - enunciate-lombok - ${enunciate.version} - - - org.mapstruct - mapstruct-processor - ${org.mapstruct.version} - - - - - - org.apache.maven.plugins - maven-assembly-plugin - - - src/main/doc/assembly.xml - - - - - package - - single - - - - - - diff --git a/scm-webapp/src/main/doc/enunciate.xml b/scm-webapp/src/main/doc/enunciate.xml deleted file mode 100644 index 225d2e0a2c..0000000000 --- a/scm-webapp/src/main/doc/enunciate.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - SCM-Manager API - - - SCM-Manager API -

    This page describes the RESTful Web Service API of SCM-Manager ${project.version}.

    - ]]> -
    - - - - - - - - - - - - - -
    diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryImportResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryImportResource.java index 40d5458812..cc2f7792c7 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryImportResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryImportResource.java @@ -1,19 +1,19 @@ /** * Copyright (c) 2010, Sebastian Sdorra * All rights reserved. - * + *

    * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + *

    * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. + * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + *

    * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE @@ -24,13 +24,11 @@ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * + *

    * http://bitbucket.org/sdorra/scm-manager - * */ - package sonia.scm.api.rest.resources; import com.google.common.base.MoreObjects; @@ -38,10 +36,6 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.io.Files; import com.google.inject.Inject; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.ResponseHeader; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.FeatureNotSupportedException; @@ -100,8 +94,7 @@ import static com.google.common.base.Preconditions.checkNotNull; * @author Sebastian Sdorra */ // @Path("import/repositories") -public class RepositoryImportResource -{ +public class RepositoryImportResource { /** * the logger for RepositoryImportResource @@ -114,13 +107,12 @@ public class RepositoryImportResource /** * Constructs a new repository import resource. * - * @param manager repository manager + * @param manager repository manager * @param serviceFactory */ @Inject public RepositoryImportResource(RepositoryManager manager, - RepositoryServiceFactory serviceFactory) - { + RepositoryServiceFactory serviceFactory) { this.manager = manager; this.serviceFactory = serviceFactory; } @@ -133,37 +125,23 @@ public class RepositoryImportResource * bundle file is passed to the {@link UnbundleCommandBuilder}. Note: This method * requires admin privileges. * - * @param uriInfo uri info - * @param type repository type - * @param name name of the repository + * @param uriInfo uri info + * @param type repository type + * @param name name of the repository * @param inputStream input bundle - * @param compressed true if the bundle is gzip compressed - * + * @param compressed true if the bundle is gzip compressed * @return empty response with location header which points to the imported repository * @since 1.43 */ @POST @Path("{type}/bundle") - @StatusCodes({ - @ResponseCode(code = 201, condition = "created", additionalHeaders = { - @ResponseHeader(name = "Location", description = "uri to the imported repository") - }), - @ResponseCode( - code = 400, - condition = "bad request, the import bundle feature is not supported by this type of repositories or the parameters are not valid" - ), - @ResponseCode(code = 409, condition = "conflict, a repository with the name already exists"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) @Consumes(MediaType.MULTIPART_FORM_DATA) public Response importFromBundle(@Context UriInfo uriInfo, - @PathParam("type") String type, @FormParam("name") String name, - @FormParam("bundle") InputStream inputStream, @QueryParam("compressed") - @DefaultValue("false") boolean compressed) - { + @PathParam("type") String type, @FormParam("name") String name, + @FormParam("bundle") InputStream inputStream, @QueryParam("compressed") + @DefaultValue("false") boolean compressed) { Repository repository = doImportFromBundle(type, name, inputStream, - compressed); + compressed); return buildResponse(uriInfo, repository); } @@ -175,43 +153,28 @@ public class RepositoryImportResource * workaround of the javascript ui extjs. Note: This method requires admin * privileges. * - * @param type repository type - * @param name name of the repository + * @param type repository type + * @param name name of the repository * @param inputStream input bundle - * @param compressed true if the bundle is gzip compressed - * + * @param compressed true if the bundle is gzip compressed * @return empty response with location header which points to the imported - * repository + * repository * @since 1.43 */ @POST @Path("{type}/bundle.html") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode( - code = 400, - condition = "bad request, the import bundle feature is not supported by this type of repositories or the parameters are not valid" - ), - @ResponseCode(code = 409, condition = "conflict, a repository with the name already exists"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(RestActionUploadResult.class) @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces(MediaType.TEXT_HTML) public Response importFromBundleUI(@PathParam("type") String type, - @FormParam("name") String name, - @FormParam("bundle") InputStream inputStream, @QueryParam("compressed") - @DefaultValue("false") boolean compressed) - { + @FormParam("name") String name, + @FormParam("bundle") InputStream inputStream, @QueryParam("compressed") + @DefaultValue("false") boolean compressed) { Response response; - try - { + try { doImportFromBundle(type, name, inputStream, compressed); response = Response.ok(new RestActionUploadResult(true)).build(); - } - catch (WebApplicationException ex) - { + } catch (WebApplicationException ex) { logger.warn("error durring bundle import", ex); response = Response.fromResponse(ex.getResponse()).entity( new RestActionUploadResult(false)).build(); @@ -227,31 +190,17 @@ public class RepositoryImportResource * repository. Note: This method requires admin privileges. * * @param uriInfo uri info - * @param type repository type + * @param type repository type * @param request request object - * * @return empty response with location header which points to the imported - * repository + * repository * @since 1.43 */ @POST @Path("{type}/url") - @StatusCodes({ - @ResponseCode(code = 201, condition = "created", additionalHeaders = { - @ResponseHeader(name = "Location", description = "uri to the imported repository") - }), - @ResponseCode( - code = 400, - condition = "bad request, the import feature is not supported by this type of repositories or the parameters are not valid" - ), - @ResponseCode(code = 409, condition = "conflict, a repository with the name already exists"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) - @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public Response importFromUrl(@Context UriInfo uriInfo, - @PathParam("type") String type, UrlImportRequest request) - { + @PathParam("type") String type, UrlImportRequest request) { RepositoryPermissions.create().check(); checkNotNull(request, "request is required"); checkArgument(!Strings.isNullOrEmpty(request.getName()), @@ -268,17 +217,12 @@ public class RepositoryImportResource Repository repository = create(type, request.getName()); RepositoryService service = null; - try - { + try { service = serviceFactory.create(repository); service.getPullCommand().pull(request.getUrl()); - } - catch (IOException ex) - { + } catch (IOException ex) { handleImportFailure(ex, repository); - } - finally - { + } finally { IOUtil.close(service); } @@ -290,23 +234,12 @@ public class RepositoryImportResource * directory. Note: This method requires admin privileges. * * @param type repository type - * * @return imported repositories */ @POST @Path("{type}") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode( - code = 400, - condition = "bad request, the import feature is not supported by this type of repositories" - ), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(Repository[].class) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public Response importRepositories(@PathParam("type") String type) - { + @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) + public Response importRepositories(@PathParam("type") String type) { RepositoryPermissions.create().check(); List repositories = new ArrayList(); @@ -315,7 +248,8 @@ public class RepositoryImportResource //J- return Response.ok( - new GenericEntity>(repositories) {} + new GenericEntity>(repositories) { + } ).build(); //J+ } @@ -327,32 +261,22 @@ public class RepositoryImportResource * @return imported repositories */ @POST - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode( - code = 400, - condition = "bad request, the import feature is not supported by this type of repositories" - ), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(Repository[].class) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public Response importRepositories() - { + @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) + public Response importRepositories() { RepositoryPermissions.create().check(); logger.info("start directory import for all supported repository types"); List repositories = new ArrayList(); - for (Type t : findImportableTypes()) - { + for (Type t : findImportableTypes()) { importFromDirectory(repositories, t.getName()); } //J- return Response.ok( - new GenericEntity>(repositories) {} + new GenericEntity>(repositories) { + } ).build(); //J+ } @@ -363,72 +287,50 @@ public class RepositoryImportResource * of failed directories. Note: This method requires admin privileges. * * @param type repository type - * * @return imported repositories * @since 1.43 */ @POST @Path("{type}/directory") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode( - code = 400, - condition = "bad request, the import feature is not supported by this type of repositories" - ), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(ImportResult.class) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public Response importRepositoriesFromDirectory( - @PathParam("type") String type) - { + @PathParam("type") String type) { RepositoryPermissions.create().check(); Response response; RepositoryHandler handler = manager.getHandler(type); - if (handler != null) - { + if (handler != null) { logger.info("start directory import for repository type {}", type); - try - { + try { ImportResult result; ImportHandler importHandler = handler.getImportHandler(); - if (importHandler instanceof AdvancedImportHandler) - { + if (importHandler instanceof AdvancedImportHandler) { logger.debug("start directory import, using advanced import handler"); result = ((AdvancedImportHandler) importHandler) .importRepositoriesFromDirectory(manager); - } - else - { + } else { logger.debug("start directory import, using normal import handler"); result = new ImportResult(importHandler.importRepositories(manager), ImmutableList.of()); } response = Response.ok(result).build(); - } - catch (FeatureNotSupportedException ex) - { + } catch (FeatureNotSupportedException ex) { logger .warn( "import feature is not supported by repository handler for type " .concat(type), ex); response = Response.status(Response.Status.BAD_REQUEST).build(); - } - catch (IOException ex) - { + } catch (IOException ex) { logger.warn("exception occured durring directory import", ex); response = Response.serverError().build(); } - } - else - { + } else { logger.warn("could not find reposiotry handler for type {}", type); response = Response.status(Response.Status.BAD_REQUEST).build(); } @@ -445,25 +347,16 @@ public class RepositoryImportResource * @return list of repository types */ @GET - @TypeHint(Type[].class) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode( - code = 400, - condition = "bad request, the import feature is not supported by this type of repositories" - ), - @ResponseCode(code = 500, condition = "internal server error") - }) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public Response getImportableTypes() - { + @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) + public Response getImportableTypes() { RepositoryPermissions.create().check(); List types = findImportableTypes(); //J- return Response.ok( - new GenericEntity>(types) {} + new GenericEntity>(types) { + } ).build(); //J+ } @@ -473,16 +366,13 @@ public class RepositoryImportResource /** * Build rest response for repository. * - * - * @param uriInfo uri info + * @param uriInfo uri info * @param repository imported repository - * * @return rest response */ - private Response buildResponse(UriInfo uriInfo, Repository repository) - { + private Response buildResponse(UriInfo uriInfo, Repository repository) { URI location = uriInfo.getBaseUriBuilder().path( - RepositoryResource.class).path(repository.getId()).build(); + RepositoryResource.class).path(repository.getId()).build(); return Response.created(location).build(); } @@ -490,15 +380,12 @@ public class RepositoryImportResource /** * Check repository type for support for the given command. * - * - * @param type repository type - * @param cmd command + * @param type repository type + * @param cmd command * @param request request object */ - private void checkSupport(Type type, Command cmd, Object request) - { - if (!(type instanceof RepositoryType)) - { + private void checkSupport(Type type, Command cmd, Object request) { + if (!(type instanceof RepositoryType)) { logger.warn("type {} is not a repository type", type.getName()); throw new WebApplicationException(Response.Status.BAD_REQUEST); @@ -506,8 +393,7 @@ public class RepositoryImportResource Set cmds = ((RepositoryType) type).getSupportedCommands(); - if (!cmds.contains(cmd)) - { + if (!cmds.contains(cmd)) { logger.warn("type {} does not support this type of import: {}", type.getName(), request); @@ -518,24 +404,18 @@ public class RepositoryImportResource /** * Creates a new repository with the given name and type. * - * * @param type repository type * @param name repository name - * * @return newly created repository */ - private Repository create(String type, String name) - { + private Repository create(String type, String name) { Repository repository = null; - try - { + try { // TODO #8783 // repository = new Repository(null, type, name); manager.create(repository); - } - catch (InternalRepositoryException ex) - { + } catch (InternalRepositoryException ex) { handleGenericCreationFailure(ex, type, name); } @@ -545,17 +425,14 @@ public class RepositoryImportResource /** * Start bundle import. * - * - * @param type repository type - * @param name name of the repository + * @param type repository type + * @param name name of the repository * @param inputStream bundle stream - * @param compressed true if the bundle is gzip compressed - * + * @param compressed true if the bundle is gzip compressed * @return imported repository */ private Repository doImportFromBundle(String type, String name, - InputStream inputStream, boolean compressed) - { + InputStream inputStream, boolean compressed) { RepositoryPermissions.create().check(); checkArgument(!Strings.isNullOrEmpty(name), @@ -564,8 +441,7 @@ public class RepositoryImportResource Repository repository; - try - { + try { Type t = type(type); checkSupport(t, Command.UNBUNDLE, "bundle"); @@ -576,26 +452,19 @@ public class RepositoryImportResource File file = File.createTempFile("scm-import-", ".bundle"); - try - { + try { long length = Files.asByteSink(file).writeFrom(inputStream); logger.info("copied {} bytes to temp, start bundle import", length); service = serviceFactory.create(repository); service.getUnbundleCommand().setCompressed(compressed).unbundle(file); - } - catch (InternalRepositoryException ex) - { + } catch (InternalRepositoryException ex) { handleImportFailure(ex, repository); - } - finally - { + } finally { IOUtil.close(service); IOUtil.delete(file); } - } - catch (IOException ex) - { + } catch (IOException ex) { logger.warn("could not create temporary file", ex); throw new WebApplicationException(ex); @@ -607,42 +476,29 @@ public class RepositoryImportResource /** * Method description * - * * @return */ - private List findImportableTypes() - { + private List findImportableTypes() { List types = new ArrayList(); Collection handlerTypes = manager.getTypes(); - for (Type t : handlerTypes) - { + for (Type t : handlerTypes) { RepositoryHandler handler = manager.getHandler(t.getName()); - if (handler != null) - { - try - { - if (handler.getImportHandler() != null) - { + if (handler != null) { + try { + if (handler.getImportHandler() != null) { types.add(t); } - } - catch (FeatureNotSupportedException ex) - { - if (logger.isTraceEnabled()) - { + } catch (FeatureNotSupportedException ex) { + if (logger.isTraceEnabled()) { logger.trace("import handler is not supported", ex); - } - else if (logger.isInfoEnabled()) - { + } else if (logger.isInfoEnabled()) { logger.info("{} handler does not support import of repositories", t.getName()); } } - } - else if (logger.isWarnEnabled()) - { + } else if (logger.isWarnEnabled()) { logger.warn("could not find handler for type {}", t.getName()); } } @@ -653,14 +509,12 @@ public class RepositoryImportResource /** * Handle creation failures. * - * - * @param ex exception + * @param ex exception * @param type repository type * @param name name of the repository */ private void handleGenericCreationFailure(Exception ex, String type, - String name) - { + String name) { logger.error(String.format("could not create repository %s with type %s", type, name), ex); @@ -670,20 +524,15 @@ public class RepositoryImportResource /** * Handle import failures. * - * - * @param ex exception + * @param ex exception * @param repository repository */ - private void handleImportFailure(Exception ex, Repository repository) - { + private void handleImportFailure(Exception ex, Repository repository) { logger.error("import for repository failed, delete repository", ex); - try - { + try { manager.delete(repository); - } - catch (InternalRepositoryException | NotFoundException e) - { + } catch (InternalRepositoryException | NotFoundException e) { logger.error("can not delete repository after import failure", e); } @@ -694,27 +543,21 @@ public class RepositoryImportResource /** * Import repositories from a specific type. * - * * @param repositories repository list - * @param type type of repository + * @param type type of repository */ - private void importFromDirectory(List repositories, String type) - { + private void importFromDirectory(List repositories, String type) { RepositoryHandler handler = manager.getHandler(type); - if (handler != null) - { + if (handler != null) { logger.info("start directory import for repository type {}", type); - try - { + try { List repositoryNames = handler.getImportHandler().importRepositories(manager); - if (repositoryNames != null) - { - for (String repositoryName : repositoryNames) - { + if (repositoryNames != null) { + for (String repositoryName : repositoryNames) { // TODO #8783 /*Repository repository = null; //manager.get(type, repositoryName); @@ -729,22 +572,14 @@ public class RepositoryImportResource }*/ } } - } - catch (FeatureNotSupportedException ex) - { + } catch (FeatureNotSupportedException ex) { throw new WebApplicationException(ex, Response.Status.BAD_REQUEST); - } - catch (IOException ex) - { + } catch (IOException ex) { + throw new WebApplicationException(ex); + } catch (InternalRepositoryException ex) { throw new WebApplicationException(ex); } - catch (InternalRepositoryException ex) - { - throw new WebApplicationException(ex); - } - } - else - { + } else { throw new WebApplicationException(Response.Status.BAD_REQUEST); } } @@ -752,17 +587,13 @@ public class RepositoryImportResource /** * Method description * - * * @param type - * * @return */ - private Type type(String type) - { + private Type type(String type) { RepositoryHandler handler = manager.getHandler(type); - if (handler == null) - { + if (handler == null) { logger.warn("no handler for type {} found", type); throw new WebApplicationException(Response.Status.NOT_FOUND); @@ -778,24 +609,21 @@ public class RepositoryImportResource */ @XmlRootElement(name = "import") @XmlAccessorType(XmlAccessType.FIELD) - public static class UrlImportRequest - { + public static class UrlImportRequest { /** * Constructs ... - * */ - public UrlImportRequest() {} + public UrlImportRequest() { + } /** * Constructs a new {@link UrlImportRequest} * - * * @param name name of the repository - * @param url external url of the repository + * @param url external url of the repository */ - public UrlImportRequest(String name, String url) - { + public UrlImportRequest(String name, String url) { this.name = name; this.url = url; } @@ -806,13 +634,12 @@ public class RepositoryImportResource * {@inheritDoc} */ @Override - public String toString() - { + public String toString() { //J- return MoreObjects.toStringHelper(this) - .add("name", name) - .add("url", url) - .toString(); + .add("name", name) + .add("url", url) + .toString(); //J+ } @@ -821,40 +648,44 @@ public class RepositoryImportResource /** * Returns name of the repository. * - * * @return name of the repository */ - public String getName() - { + public String getName() { return name; } /** * Returns external url of the repository. * - * * @return external url of the repository */ - public String getUrl() - { + public String getUrl() { return url; } //~--- fields ------------------------------------------------------------- - /** name of the repository */ + /** + * name of the repository + */ private String name; - /** external url of the repository */ + /** + * external url of the repository + */ private String url; } //~--- fields --------------------------------------------------------------- - /** repository manager */ + /** + * repository manager + */ private final RepositoryManager manager; - /** repository service factory */ + /** + * repository service factory + */ private final RepositoryServiceFactory serviceFactory; } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java index b18be9207e..33c6fb0908 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java @@ -1,9 +1,8 @@ package sonia.scm.api.v2.resources; import com.google.common.base.Strings; -import com.webcohesion.enunciate.metadata.rs.ResponseHeader; -import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -181,7 +180,14 @@ public class BranchRootResource { @Path("") @Consumes(VndMediaType.BRANCH_REQUEST) @Operation(summary = "Create branch", description = "Creates a new branch.", tags = "Repository") - @ApiResponse(responseCode = "201", description = "create success") + @ApiResponse( + responseCode = "201", + description = "create success", + headers = @Header( + name = "Location", + description = "uri to the created branch" + ) + ) @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"push\" privilege") @ApiResponse(responseCode = "409", description = "conflict, a branch with this name already exists") @@ -192,7 +198,6 @@ public class BranchRootResource { mediaType = VndMediaType.ERROR_TYPE, schema = @Schema(implementation = ErrorDto.class) )) - @ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created branch")) public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid BranchRequestDto branchRequest) throws IOException { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java index cfb498dcb3..455a82c4f9 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java @@ -1,8 +1,7 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseHeader; -import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -97,7 +96,14 @@ public class GroupCollectionResource { @Path("") @Consumes(VndMediaType.GROUP) @Operation(summary = "Create group", description = "Creates a new group.", tags = "Group") - @ApiResponse(responseCode = "201", description = "create success") + @ApiResponse( + responseCode = "201", + description = "create success", + headers = @Header( + name = "Location", + description = "uri to the created group" + ) + ) @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"group\" privilege") @ApiResponse(responseCode = "409", description = "conflict, a group with this name already exists") @@ -109,7 +115,6 @@ public class GroupCollectionResource { schema = @Schema(implementation = ErrorDto.class) ) ) - @ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created group")) public Response create(@Valid GroupDto group) { return adapter.create(group, () -> dtoToGroupMapper.map(group), diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java index f4eb28770f..d7fd6a79a4 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java @@ -1,8 +1,7 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseHeader; -import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -105,7 +104,14 @@ public class RepositoryCollectionResource { @Path("") @Consumes(VndMediaType.REPOSITORY) @Operation(summary = "Create repository", description = "Creates a new repository.", tags = "Repository") - @ApiResponse(responseCode = "201", description = "create success") + @ApiResponse( + responseCode = "201", + description = "create success", + headers = @Header( + name = "Location", + description = "uri to the created repository" + ) + ) @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"repository\" privilege") @ApiResponse(responseCode = "409", description = "conflict, a repository with this name already exists") @@ -116,7 +122,6 @@ public class RepositoryCollectionResource { mediaType = VndMediaType.ERROR_TYPE, schema = @Schema(implementation = ErrorDto.class) )) - @ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created repository")) public Response create(@Valid RepositoryDto repository, @QueryParam("initialize") boolean initialize) { AtomicReference reference = new AtomicReference<>(); Response response = adapter.create(repository, diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleCollectionResource.java index 60f39f774f..a039b0cb35 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleCollectionResource.java @@ -1,8 +1,7 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseHeader; -import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -72,12 +71,12 @@ public class RepositoryRoleCollectionResource { schema = @Schema(implementation = ErrorDto.class) )) public Response getAll(@DefaultValue("0") @QueryParam("page") int page, - @DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize, - @QueryParam("sortBy") String sortBy, - @DefaultValue("false") @QueryParam("desc") boolean desc + @DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize, + @QueryParam("sortBy") String sortBy, + @DefaultValue("false") @QueryParam("desc") boolean desc ) { return adapter.getAll(page, pageSize, x -> true, sortBy, desc, - pageResult -> repositoryRoleCollectionToDtoMapper.map(page, pageSize, pageResult)); + pageResult -> repositoryRoleCollectionToDtoMapper.map(page, pageSize, pageResult)); } /** @@ -92,7 +91,14 @@ public class RepositoryRoleCollectionResource { @Path("") @Consumes(VndMediaType.REPOSITORY_ROLE) @Operation(summary = "Create repository role", description = "Creates a new repository role.", tags = "Repository role") - @ApiResponse(responseCode = "201", description = "create success") + @ApiResponse( + responseCode = "201", + description = "create success", + headers = @Header( + name = "Location", + description = "uri to the created repository role" + ) + ) @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"repositoryRole\" privilege") @ApiResponse(responseCode = "409", description = "conflict, a repository role with this name already exists") @@ -103,7 +109,6 @@ public class RepositoryRoleCollectionResource { mediaType = VndMediaType.ERROR_TYPE, schema = @Schema(implementation = ErrorDto.class) )) - @ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created repositoryRole")) public Response create(@Valid RepositoryRoleDto repositoryRole) { return adapter.create(repositoryRole, () -> dtoToRepositoryRoleMapper.map(repositoryRole), u -> resourceLinks.repositoryRole().self(u.getName())); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java index 72c0a534ef..ef8b230f8a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java @@ -1,8 +1,7 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseHeader; -import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -80,13 +79,13 @@ public class UserCollectionResource { schema = @Schema(implementation = ErrorDto.class) )) public Response getAll(@DefaultValue("0") @QueryParam("page") int page, - @DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize, - @QueryParam("sortBy") String sortBy, - @DefaultValue("false") @QueryParam("desc") boolean desc, - @DefaultValue("") @QueryParam("q") String search + @DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize, + @QueryParam("sortBy") String sortBy, + @DefaultValue("false") @QueryParam("desc") boolean desc, + @DefaultValue("") @QueryParam("q") String search ) { return adapter.getAll(page, pageSize, createSearchPredicate(search), sortBy, desc, - pageResult -> userCollectionToDtoMapper.map(page, pageSize, pageResult)); + pageResult -> userCollectionToDtoMapper.map(page, pageSize, pageResult)); } /** @@ -101,7 +100,14 @@ public class UserCollectionResource { @Path("") @Consumes(VndMediaType.USER) @Operation(summary = "Create user", description = "Creates a new user.", tags = "User") - @ApiResponse(responseCode = "201", description = "create success") + @ApiResponse( + responseCode = "201", + description = "create success", + headers = @Header( + name = "Location", + description = "uri to the created user" + ) + ) @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"user\" privilege") @ApiResponse(responseCode = "409", description = "conflict, a user with this name already exists") @@ -112,7 +118,6 @@ public class UserCollectionResource { mediaType = VndMediaType.ERROR_TYPE, schema = @Schema(implementation = ErrorDto.class) )) - @ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created user")) public Response create(@Valid UserDto user) { return adapter.create(user, () -> dtoToUserMapper.map(user, passwordService.encryptPassword(user.getPassword())), u -> resourceLinks.user().self(u.getName())); } From a016710c3531305247a062954ce76b6447250565 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Mon, 24 Feb 2020 15:02:03 +0100 Subject: [PATCH 37/46] Sorted extension point entries with supplied extensionName --- CHANGELOG.md | 3 ++ .../src/config/ConfigurationBinder.tsx | 2 +- scm-ui/ui-extensions/src/binder.test.ts | 49 +++++++++++++------ scm-ui/ui-extensions/src/binder.ts | 28 ++++++++++- 4 files changed, 65 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd845f009c..3f40686a7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added +- Extension point entries with supplied extensionName are sorted ascending + ### Fixed - Modification for mercurial repositories with enabled XSRF protection diff --git a/scm-ui/ui-components/src/config/ConfigurationBinder.tsx b/scm-ui/ui-components/src/config/ConfigurationBinder.tsx index f2e3669a5e..d99c3d12ce 100644 --- a/scm-ui/ui-components/src/config/ConfigurationBinder.tsx +++ b/scm-ui/ui-components/src/config/ConfigurationBinder.tsx @@ -41,7 +41,7 @@ class ConfigurationBinder { }); // bind navigation link to extension point - binder.bind("admin.setting", ConfigNavLink, configPredicate); + binder.bind("admin.setting", ConfigNavLink, configPredicate, labelI18nKey); // route for global configuration, passes the link from the index resource to component const ConfigRoute = ({ url, links, ...additionalProps }: GlobalRouteProps) => { diff --git a/scm-ui/ui-extensions/src/binder.test.ts b/scm-ui/ui-extensions/src/binder.test.ts index 2bcd302a36..18cd969eb3 100644 --- a/scm-ui/ui-extensions/src/binder.test.ts +++ b/scm-ui/ui-extensions/src/binder.test.ts @@ -13,31 +13,31 @@ describe("binder tests", () => { }); it("should return the binded extensions", () => { - binder.bind("hitchhicker.trillian", "heartOfGold"); - binder.bind("hitchhicker.trillian", "earth"); + binder.bind("hitchhiker.trillian", "heartOfGold"); + binder.bind("hitchhiker.trillian", "earth"); - const extensions = binder.getExtensions("hitchhicker.trillian"); + const extensions = binder.getExtensions("hitchhiker.trillian"); expect(extensions).toEqual(["heartOfGold", "earth"]); }); it("should return the first bound extension", () => { - binder.bind("hitchhicker.trillian", "heartOfGold"); - binder.bind("hitchhicker.trillian", "earth"); + binder.bind("hitchhiker.trillian", "heartOfGold"); + binder.bind("hitchhiker.trillian", "earth"); - expect(binder.getExtension("hitchhicker.trillian")).toBe("heartOfGold"); + expect(binder.getExtension("hitchhiker.trillian")).toBe("heartOfGold"); }); it("should return null if no extension was bound", () => { - expect(binder.getExtension("hitchhicker.trillian")).toBe(null); + expect(binder.getExtension("hitchhiker.trillian")).toBe(null); }); it("should return true, if an extension is bound", () => { - binder.bind("hitchhicker.trillian", "heartOfGold"); - expect(binder.hasExtension("hitchhicker.trillian")).toBe(true); + binder.bind("hitchhiker.trillian", "heartOfGold"); + expect(binder.hasExtension("hitchhiker.trillian")).toBe(true); }); it("should return false, if no extension is bound", () => { - expect(binder.hasExtension("hitchhicker.trillian")).toBe(false); + expect(binder.hasExtension("hitchhiker.trillian")).toBe(false); }); type Props = { @@ -45,13 +45,34 @@ describe("binder tests", () => { }; it("should return only extensions which predicates matches", () => { - binder.bind("hitchhicker.trillian", "heartOfGold", (props: Props) => props.category === "a"); - binder.bind("hitchhicker.trillian", "earth", (props: Props) => props.category === "b"); - binder.bind("hitchhicker.trillian", "earth2", (props: Props) => props.category === "a"); + binder.bind("hitchhiker.trillian", "heartOfGold", (props: Props) => props.category === "a"); + binder.bind("hitchhiker.trillian", "earth", (props: Props) => props.category === "b"); + binder.bind("hitchhiker.trillian", "earth2", (props: Props) => props.category === "a"); - const extensions = binder.getExtensions("hitchhicker.trillian", { + const extensions = binder.getExtensions("hitchhiker.trillian", { category: "b" }); expect(extensions).toEqual(["earth"]); }); + + it("should return extensions in ascending order", () => { + binder.bind("hitchhiker.trillian", "planetA", () => true, "zeroWaste"); + binder.bind("hitchhiker.trillian", "planetB", () => true, "EPSILON"); + binder.bind("hitchhiker.trillian", "planetC", () => true, "emptyBin"); + binder.bind("hitchhiker.trillian", "planetD", () => true, "absolute"); + + const extensions = binder.getExtensions("hitchhiker.trillian"); + expect(extensions).toEqual(["planetD", "planetC", "planetB", "planetA"]); + }); + + it("should return extensions starting with entries with specified extensionName", () => { + binder.bind("hitchhiker.trillian", "planetA", () => true); + binder.bind("hitchhiker.trillian", "planetB", () => true, "zeroWaste"); + binder.bind("hitchhiker.trillian", "planetC", () => true); + binder.bind("hitchhiker.trillian", "planetD", () => true, "emptyBin"); + + const extensions = binder.getExtensions("hitchhiker.trillian"); + expect(extensions[0]).toEqual("planetD"); + expect(extensions[1]).toEqual("planetB"); + }); }); diff --git a/scm-ui/ui-extensions/src/binder.ts b/scm-ui/ui-extensions/src/binder.ts index a359973a50..e0077b0b67 100644 --- a/scm-ui/ui-extensions/src/binder.ts +++ b/scm-ui/ui-extensions/src/binder.ts @@ -3,6 +3,7 @@ type Predicate = (props: any) => boolean; type ExtensionRegistration = { predicate: Predicate; extension: any; + extensionName: string; }; /** @@ -25,13 +26,14 @@ export class Binder { * @param extension provided extension * @param predicate to decide if the extension gets rendered for the given props */ - bind(extensionPoint: string, extension: any, predicate?: Predicate) { + bind(extensionPoint: string, extension: any, predicate?: Predicate, extensionName?: string) { if (!this.extensionPoints[extensionPoint]) { this.extensionPoints[extensionPoint] = []; } const registration = { predicate: predicate ? predicate : () => true, - extension + extension, + extensionName: extensionName ? extensionName : "" }; this.extensionPoints[extensionPoint].push(registration); } @@ -61,6 +63,7 @@ export class Binder { if (props) { registrations = registrations.filter(reg => reg.predicate(props || {})); } + registrations.sort(this.sortExtensions); return registrations.map(reg => reg.extension); } @@ -70,6 +73,27 @@ export class Binder { hasExtension(extensionPoint: string, props?: object): boolean { return this.getExtensions(extensionPoint, props).length > 0; } + + /** + * Sort extensions in ascending order. + */ + sortExtensions = (a: ExtensionRegistration, b: ExtensionRegistration) => { + const regA = a.extensionName ? a.extensionName.toUpperCase() : ""; + const regB = b.extensionName ? b.extensionName.toUpperCase() : ""; + + if (regA === "" && regB === "") { + return 0; + } else if (regA === "") { + return 1; + } else if (regB === "") { + return -1; + } else if (regA > regB) { + return 1; + } else if (regA < regB) { + return -1; + } + return 0; + }; } // singleton binder From a5f27adc71073ef138c7a6da56b8ddadb5fc6aa0 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Mon, 24 Feb 2020 15:05:13 +0100 Subject: [PATCH 38/46] Described sort method in a more understandable way --- scm-ui/ui-extensions/src/binder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui/ui-extensions/src/binder.ts b/scm-ui/ui-extensions/src/binder.ts index e0077b0b67..32c814b8fd 100644 --- a/scm-ui/ui-extensions/src/binder.ts +++ b/scm-ui/ui-extensions/src/binder.ts @@ -75,7 +75,7 @@ export class Binder { } /** - * Sort extensions in ascending order. + * Sort extensions in ascending order, starting with entries with specified extensionName. */ sortExtensions = (a: ExtensionRegistration, b: ExtensionRegistration) => { const regA = a.extensionName ? a.extensionName.toUpperCase() : ""; From 36469d64a1bfa59704b75441957b748bab830d89 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Mon, 24 Feb 2020 15:21:05 +0100 Subject: [PATCH 39/46] Describe REST API in Swagger UI --- scm-webapp/src/main/doc/openapi.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/scm-webapp/src/main/doc/openapi.md b/scm-webapp/src/main/doc/openapi.md index c014899c30..949dbd03a0 100644 --- a/scm-webapp/src/main/doc/openapi.md +++ b/scm-webapp/src/main/doc/openapi.md @@ -1,3 +1,17 @@ -# openapi docs from code +# OpenAPI REST documentation -describe hateoas +The following REST documentation describes all public endpoints of your SCM-Manager instance. +You can try the endpoints with or without authentication right on the swagger surface provided by the OpenAPI-Plugin. + +For authenticated requests please use the "Authorize" button and insert your preferred authentication method. +For basic authentication simply use your SCM-Manager credentials. If you want to use the bearer token authentication, you can generate an +valid token using the authentication endpoint and copy the response body. + +SCM-Manager defines a modern ["Level 3"-REST API](https://martinfowler.com/articles/richardsonMaturityModel.html). +Using the HATEOAS architecture for REST allows us to provide discoverable and self explanatory endpoint definitions. +The responses are build using the [HAL format](http://stateless.co/hal_specification.html) as JSON or XML. +HAL makes the API human-friendly and simplifies the communication between the frontend and the server using links and embedded resources. + +We highly suggest using the HAL links when creating new functions for the SCM-Manager since they are consistent and can be +permission checked before being append to the response. The links and embedded resources can also be used by plugins, which can +define new resources or enrich existing ones. From d85cfc23e216ca68c91552bf65ac778b898d284c Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Tue, 25 Feb 2020 07:34:31 +0000 Subject: [PATCH 40/46] fix typo in Changelog.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa1a8ed5aa..760da98469 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added footer extension points for links and avatar -- Create OpenAPI specification durring build +- Create OpenAPI specification during build ### Changed - New footer design From a557997fa4fc736061885125f73bc65dbb0ea4e0 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Tue, 25 Feb 2020 09:18:29 +0100 Subject: [PATCH 41/46] Simplify sort extensions method --- scm-ui/ui-extensions/src/binder.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/scm-ui/ui-extensions/src/binder.ts b/scm-ui/ui-extensions/src/binder.ts index 32c814b8fd..693fbb655a 100644 --- a/scm-ui/ui-extensions/src/binder.ts +++ b/scm-ui/ui-extensions/src/binder.ts @@ -81,11 +81,9 @@ export class Binder { const regA = a.extensionName ? a.extensionName.toUpperCase() : ""; const regB = b.extensionName ? b.extensionName.toUpperCase() : ""; - if (regA === "" && regB === "") { - return 0; - } else if (regA === "") { + if (regA === "" && regB !== "") { return 1; - } else if (regB === "") { + } else if (regA !== "" && regB === "") { return -1; } else if (regA > regB) { return 1; From b3a9b8a42ccf3695c5d00f790e71a0718fa07282 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 25 Feb 2020 10:10:15 +0100 Subject: [PATCH 42/46] remove heading, because the one from swagger-ui is enough --- scm-webapp/src/main/doc/openapi.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/scm-webapp/src/main/doc/openapi.md b/scm-webapp/src/main/doc/openapi.md index 949dbd03a0..47ed0c1c83 100644 --- a/scm-webapp/src/main/doc/openapi.md +++ b/scm-webapp/src/main/doc/openapi.md @@ -1,5 +1,3 @@ -# OpenAPI REST documentation - The following REST documentation describes all public endpoints of your SCM-Manager instance. You can try the endpoints with or without authentication right on the swagger surface provided by the OpenAPI-Plugin. From 039ea46958e34e764c130538554a9977fddbef14 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 25 Feb 2020 10:12:04 +0100 Subject: [PATCH 43/46] improve openapi description --- scm-webapp/src/main/doc/openapi.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scm-webapp/src/main/doc/openapi.md b/scm-webapp/src/main/doc/openapi.md index 47ed0c1c83..bf565699d4 100644 --- a/scm-webapp/src/main/doc/openapi.md +++ b/scm-webapp/src/main/doc/openapi.md @@ -7,9 +7,9 @@ valid token using the authentication endpoint and copy the response body. SCM-Manager defines a modern ["Level 3"-REST API](https://martinfowler.com/articles/richardsonMaturityModel.html). Using the HATEOAS architecture for REST allows us to provide discoverable and self explanatory endpoint definitions. -The responses are build using the [HAL format](http://stateless.co/hal_specification.html) as JSON or XML. +The responses are build using the [HAL JSON format](http://stateless.co/hal_specification.html). HAL makes the API human-friendly and simplifies the communication between the frontend and the server using links and embedded resources. -We highly suggest using the HAL links when creating new functions for the SCM-Manager since they are consistent and can be +We highly suggest using HAL links when creating new functions for SCM-Manager since they are consistent and can be permission checked before being append to the response. The links and embedded resources can also be used by plugins, which can define new resources or enrich existing ones. From 2fdd7f31ffcdc0ff16acb2480adff29728cc597b Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 25 Feb 2020 09:12:45 +0000 Subject: [PATCH 44/46] Close branch feature/openapi_doc From 412cae3c21f6b57b8befaf3481b4da9a3ba82fa3 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 25 Feb 2020 11:12:04 +0100 Subject: [PATCH 45/46] do not try to archive restdocs package, because it does not exists anymore --- Jenkinsfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 43fece6567..8218808237 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -29,7 +29,7 @@ node('docker') { } stage('Build') { - mvn 'clean install -Pdoc -DskipTests' + mvn 'clean install -DskipTests' } stage('Unit Test') { @@ -67,7 +67,6 @@ node('docker') { stage('Archive') { archiveArtifacts 'scm-webapp/target/scm-webapp.war' archiveArtifacts 'scm-server/target/scm-server-app.*' - archiveArtifacts 'scm-webapp/target/scm-webapp-restdocs.zip' } stage('Docker') { From 2b0d4a65a078a59d0f7d51c70e9d331ad4bbffd4 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 25 Feb 2020 15:23:56 +0000 Subject: [PATCH 46/46] Close branch feature/sort_extensionpoint_entries

    Name Description