diff --git a/CHANGELOG.md b/CHANGELOG.md index d4dcc480d1..a715533bac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ 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 +- Create OpenAPI specification during build +- Extension point entries with supplied extensionName are sorted ascending + +### Changed +- New footer design + +### Fixed +- Modification for mercurial repositories with enabled XSRF protection + +### Removed +- Enunciate rest documentation + ## 2.0.0-rc4 - 2020-02-14 ### Added - Support for Java versions > 8 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') { diff --git a/pom.xml b/pom.xml index 48e94c11de..41bd917705 100644 --- a/pom.xml +++ b/pom.xml @@ -184,12 +184,6 @@ true - - com.webcohesion.enunciate - enunciate-core-annotations - ${enunciate.version} - - org.mapstruct mapstruct-jdk8 @@ -266,6 +260,12 @@ ${jaxrs.version} + + io.swagger.core.v3 + swagger-annotations + 2.1.1 + + com.fasterxml.jackson.core jackson-core @@ -447,16 +447,10 @@ 2.3 - - com.webcohesion.enunciate - enunciate-maven-plugin - ${enunciate.version} - - sonia.scm.maven smp-maven-plugin - 1.0.0-rc3 + 1.0.0-rc4 @@ -465,6 +459,12 @@ 2.8.2 + + io.openapitools.swagger + swagger-maven-plugin + 2.1.2 + + @@ -831,7 +831,6 @@ 2.1.1 4.4.1.Final 1.19.4 - 2.11.1 2.10.0 4.0 2.3.0 @@ -856,8 +855,8 @@ 26.0-jre - 10.16.0 - 1.16.0 + 12.16.1 + 1.22.0 8 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-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-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/pom.xml b/scm-plugins/pom.xml index e6b2929bd2..57999aa7d0 100644 --- a/scm-plugins/pom.xml +++ b/scm-plugins/pom.xml @@ -61,6 +61,13 @@ provided + + + io.swagger.core.v3 + swagger-annotations + provided + + @@ -136,100 +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 + + + + - - - 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-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..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,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.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 +18,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 +51,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(); @@ -74,13 +91,20 @@ 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") - }) - @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 +118,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..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,9 @@ 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 +13,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 +45,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 +82,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-plugins/scm-hg-plugin/pom.xml b/scm-plugins/scm-hg-plugin/pom.xml index e57652bf0f..c89d7a085d 100644 --- a/scm-plugins/scm-hg-plugin/pom.xml +++ b/scm-plugins/scm-hg-plugin/pom.xml @@ -27,6 +27,7 @@ + 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-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..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 @@ -38,6 +38,9 @@ package sonia.scm.repository; import com.google.inject.ProvisionException; 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; @@ -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 --------------------------------------------------------- /** @@ -114,8 +119,9 @@ public final class HgEnvironment } try { - String credentials = hookManager.getCredentials(); - environment.put(SCM_BEARER_TOKEN, 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); } @@ -123,4 +129,8 @@ public final class HgEnvironment environment.put(ENV_URL, hookUrl); environment.put(ENV_CHALLENGE, hookManager.getChallenge()); } + + 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 6815bdad96..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 CipherUtil.getInstance().encode(accessToken.compact()); + return accessTokenBuilderFactory.create().build(); } //~--- methods -------------------------------------------------------------- @@ -279,7 +276,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..2718e0b899 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgEnvironmentTest.java @@ -0,0 +1,54 @@ +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 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 { + + @Mock + HgRepositoryHandler handler; + @Mock + HgHookManager hookManager; + + @Test + void shouldExtractXsrfTokenWhenSet() { + 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); + + assertThat(environment).contains(entry("SCM_XSRF", "XSRF Token")); + } + + @Test + void shouldIgnoreXsrfWhenNotSetButStillContainDummy() { + 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); + + assertThat(environment).containsKeys("SCM_XSRF"); + } +} 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; } 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-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); diff --git a/scm-ui/ui-components/src/__resources__/hitchhiker.png b/scm-ui/ui-components/src/__resources__/hitchhiker.png new file mode 100644 index 0000000000..71632a3a51 Binary files /dev/null and b/scm-ui/ui-components/src/__resources__/hitchhiker.png differ 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 0000000000..a98f6b09cd Binary files /dev/null and b/scm-ui/ui-components/src/__resources__/marvin.jpg differ 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 0ec9bb0c76..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`] = `