Merge with default

This commit is contained in:
Rene Pfeuffer
2020-03-03 09:46:54 +01:00
88 changed files with 3434 additions and 1551 deletions

View File

@@ -5,12 +5,22 @@ 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
- Update svnkit to version 1.10.1-scm1
### Fixed
- Modification for mercurial repositories with enabled XSRF protection
- Does not throw NullPointerException when merge fails without normal merge conflicts
- Keep file attributes on modification
### Removed
- Enunciate rest documentation
## 2.0.0-rc4 - 2020-02-14
### Added
@@ -22,7 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Use icon only buttons for diff file controls
- Upgrade [Legman](https://github.com/sdorra/legman) to v1.6.2 in order to fix execution on Java versions > 8
- Upgrade [Lombok](https://projectlombok.org/) to version 1.18.10 in order to fix build on Java versions > 8
- Upgrade [Mockito](https://site.mockito.org/) to version 2.28.2 in order to fix tests on Java versions > 8
- 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

10
Jenkinsfile vendored
View File

@@ -29,11 +29,11 @@ node('docker') {
}
stage('Build') {
mvn 'clean install -Pdoc -DskipTests'
mvn 'clean install -DskipTests'
}
stage('Unit Test') {
mvn 'test -Pcoverage -Dsonia.scm.test.skip.hg=true -Dmaven.test.failure.ignore=true'
mvn 'test -Pcoverage -Dmaven.test.failure.ignore=true'
}
stage('Integration 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') {
@@ -97,9 +96,6 @@ node('docker') {
// Archive Unit and integration test results, if any
junit allowEmptyResults: true, testResults: '**/target/failsafe-reports/TEST-*.xml,**/target/surefire-reports/TEST-*.xml,**/target/jest-reports/TEST-*.xml'
// Find maven warnings and visualize in job
warnings consoleParsers: [[parserName: 'Maven']], canRunOnFailed: true
mailIfStatusChanged(commitAuthorEmail)
}
}
@@ -108,7 +104,7 @@ String mainBranch
Maven setupMavenBuild() {
// Keep this version number in sync with .mvn/maven-wrapper.properties
Maven mvn = new MavenInDocker(this, '3.6.3-jdk-11')
Maven mvn = new MavenWrapper(this)
if (isMainBranch()) {
// Release starts javadoc, which takes very long, so do only for certain branches

31
pom.xml
View File

@@ -184,12 +184,6 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.webcohesion.enunciate</groupId>
<artifactId>enunciate-core-annotations</artifactId>
<version>${enunciate.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-jdk8</artifactId>
@@ -266,6 +260,12 @@
<version>${jaxrs.version}</version>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
@@ -447,16 +447,10 @@
<version>2.3</version>
</plugin>
<plugin>
<groupId>com.webcohesion.enunciate</groupId>
<artifactId>enunciate-maven-plugin</artifactId>
<version>${enunciate.version}</version>
</plugin>
<plugin>
<groupId>sonia.scm.maven</groupId>
<artifactId>smp-maven-plugin</artifactId>
<version>1.0.0-rc3</version>
<version>1.0.0-rc4</version>
</plugin>
<plugin>
@@ -465,6 +459,12 @@
<version>2.8.2</version>
</plugin>
<plugin>
<groupId>io.openapitools.swagger</groupId>
<artifactId>swagger-maven-plugin</artifactId>
<version>2.1.2</version>
</plugin>
</plugins>
</pluginManagement>
@@ -831,7 +831,6 @@
<jaxrs.version>2.1.1</jaxrs.version>
<resteasy.version>4.4.1.Final</resteasy.version>
<jersey-client.version>1.19.4</jersey-client.version>
<enunciate.version>2.11.1</enunciate.version>
<jackson.version>2.10.0</jackson.version>
<guice.version>4.0</guice.version>
<jaxb.version>2.3.0</jaxb.version>
@@ -856,8 +855,8 @@
<guava.version>26.0-jre</guava.version>
<!-- frontend -->
<nodejs.version>10.16.0</nodejs.version>
<yarn.version>1.16.0</yarn.version>
<nodejs.version>12.16.1</nodejs.version>
<yarn.version>1.22.0</yarn.version>
<!-- build properties -->
<project.build.javaLevel>8</project.build.javaLevel>

View File

@@ -137,12 +137,6 @@
<scope>provided</scope>
</dependency>
<!-- rest documentation -->
<dependency>
<groupId>com.webcohesion.enunciate</groupId>
<artifactId>enunciate-core-annotations</artifactId>
</dependency>
<!-- event bus -->
<dependency>

View File

@@ -1,6 +1,8 @@
package sonia.scm.repository.spi;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.ContextEntry;
import sonia.scm.repository.Repository;
@@ -22,6 +24,8 @@ import static sonia.scm.NotFoundException.notFound;
*/
public interface ModifyWorkerHelper extends ModifyCommand.Worker {
Logger LOG = LoggerFactory.getLogger(ModifyWorkerHelper.class);
@Override
default void delete(String toBeDeleted) throws IOException {
Path fileToBeDeleted = new File(getWorkDir(), toBeDeleted).toPath();
@@ -57,7 +61,11 @@ public interface ModifyWorkerHelper extends ModifyCommand.Worker {
if (!targetFile.toFile().exists()) {
throw notFound(createFileContext(path));
}
boolean executable = Files.isExecutable(targetFile);
Files.move(file.toPath(), targetFile, REPLACE_EXISTING);
if (targetFile.toFile().setExecutable(executable)) {
LOG.warn("could not set executable flag for file {}", targetFile);
}
addFileToScm(path, targetFile);
}

View File

@@ -0,0 +1,74 @@
package sonia.scm.repository.spi;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import sonia.scm.repository.Repository;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(TempDirectory.class)
class ModifyWorkerHelperTest {
@Test
void shouldKeepExecutableFlag(@TempDirectory.TempDir Path temp) throws IOException {
File target = createFile(temp, "executable.sh");
File newFile = createFile(temp, "other");
target.setExecutable(true);
ModifyWorkerHelper helper = new MinimalModifyWorkerHelper(temp);
helper.modify("executable.sh", newFile);
assertThat(target.canExecute()).isTrue();
}
private File createFile(Path temp, String fileName) throws IOException {
File file = new File(temp.toFile(), fileName);
FileWriter source = new FileWriter(file);
source.write("something");
source.close();
return file;
}
private static class MinimalModifyWorkerHelper implements ModifyWorkerHelper {
private final Path temp;
public MinimalModifyWorkerHelper(Path temp) {
this.temp = temp;
}
@Override
public void doScmDelete(String toBeDeleted) {
}
@Override
public void addFileToScm(String name, Path file) {
}
@Override
public File getWorkDir() {
return temp.toFile();
}
@Override
public Repository getRepository() {
return null;
}
@Override
public String getBranch() {
return null;
}
}
}

View File

@@ -61,6 +61,13 @@
<scope>provided</scope>
</dependency>
<!-- openapi documentation -->
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<scope>provided</scope>
</dependency>
<!-- test scope -->
<dependency>
@@ -136,100 +143,37 @@
</configuration>
</plugin>
<plugin>
<groupId>io.openapitools.swagger</groupId>
<artifactId>swagger-maven-plugin</artifactId>
<configuration>
<resourcePackages>
<resourcePackage>sonia.scm.api.v2.resources</resourcePackage>
</resourcePackages>
<outputDirectory>${basedir}/target/classes/META-INF/scm</outputDirectory>
<outputFilename>openapi</outputFilename>
<outputFormats>JSON,YAML</outputFormats>
<prettyPrint>true</prettyPrint>
<swaggerConfig>
<info>
<title>SCM-Manager Plugin REST-API</title>
<version>${project.version}</version>
<license>
<url>http://www.opensource.org/licenses/bsd-license.php</url>
<name>BSD</name>
</license>
</info>
</swaggerConfig>
</configuration>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>plugin-doc</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>copy-enunciate-configuration</id>
<phase>compile</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}</outputDirectory>
<resources>
<resource>
<directory>src/main/doc</directory>
<filtering>true</filtering>
<includes>
<include>**/enunciate.xml</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.webcohesion.enunciate</groupId>
<artifactId>enunciate-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>docs</goal>
</goals>
<phase>compile</phase>
</execution>
</executions>
<configuration>
<configFile>${project.build.directory}/enunciate.xml</configFile>
<docsDir>${project.build.directory}</docsDir>
<docsSubdir>restdocs</docsSubdir>
</configuration>
<dependencies>
<dependency>
<groupId>com.webcohesion.enunciate</groupId>
<artifactId>enunciate-top</artifactId>
<version>${enunciate.version}</version>
<exclusions>
<exclusion>
<groupId>com.webcohesion.enunciate</groupId>
<artifactId>enunciate-swagger</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.webcohesion.enunciate</groupId>
<artifactId>enunciate-lombok</artifactId>
<version>${enunciate.version}</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptors>
<descriptor>src/main/doc/assembly.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View File

@@ -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();
}
}

View File

@@ -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();

View File

@@ -88,7 +88,14 @@ abstract class GitMergeStrategy extends AbstractGitCommand.GitCloneWorker<MergeC
}
MergeCommandResult analyseFailure(MergeResult result) {
logger.info("could not merge branch {} into {} due to conflict in paths {}", branchToMerge, targetBranch, result.getConflicts().keySet());
logger.info("could not merge branch {} into {} with merge status '{}' due to ...", branchToMerge, targetBranch, result.getMergeStatus());
logger.info("... conflicts: {}", result.getConflicts());
logger.info("... checkout conflicts: {}", result.getCheckoutConflicts());
logger.info("... failing paths: {}", result.getFailingPaths());
logger.info("... message: {}", result);
if (result.getConflicts() == null) {
throw new UnexpectedMergeResultException(getRepository(), result);
}
return MergeCommandResult.failure(targetRevision.name(), revisionToMerge.name(), result.getConflicts().keySet());
}
}

View File

@@ -0,0 +1,27 @@
package sonia.scm.repository.spi;
import org.eclipse.jgit.api.MergeResult;
import sonia.scm.ContextEntry;
import sonia.scm.ExceptionWithContext;
import sonia.scm.repository.Repository;
class UnexpectedMergeResultException extends ExceptionWithContext {
public static final String CODE = "4GRrgkSC01";
public UnexpectedMergeResultException(Repository repository, MergeResult result) {
super(ContextEntry.ContextBuilder.entity(repository).build(), createMessage(result));
}
private static String createMessage(MergeResult result) {
return "unexpected merge result: " + result
+ "\nconflicts: " + result.getConflicts()
+ "\ncheckout conflicts: " + result.getCheckoutConflicts()
+ "\nfailing paths: " + result.getFailingPaths();
}
@Override
public String getCode() {
return CODE;
}
}

View File

@@ -113,5 +113,4 @@ public class AbstractGitCommandTestBase extends ZippedRepositoryTestBase
/** Field description */
private GitContext context;
private ScmTransportProtocol scmTransportProtocol;
}

View File

@@ -12,16 +12,23 @@ import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Rule;
import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import sonia.scm.NoChangesMadeException;
import sonia.scm.NotFoundException;
import sonia.scm.repository.GitWorkdirFactory;
import sonia.scm.repository.Person;
import sonia.scm.repository.api.MergeCommandResult;
import sonia.scm.repository.api.MergeStrategy;
import sonia.scm.repository.util.WorkdirProvider;
import sonia.scm.user.User;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import static org.assertj.core.api.Assertions.assertThat;
@@ -163,11 +170,34 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
assertThat(mergeCommandResult.getFilesWithConflict()).containsExactly("a.txt");
}
@Test
public void shouldHandleUnexpectedMergeResults() {
GitMergeCommand command = createCommand(git -> {
try {
FileWriter fw = new FileWriter(new File(git.getRepository().getWorkTree(), "b.txt"), true);
BufferedWriter bw = new BufferedWriter(fw);
bw.write("change");
bw.newLine();
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
});
MergeCommandRequest request = new MergeCommandRequest();
request.setBranchToMerge("mergeable");
request.setTargetBranch("master");
request.setMergeStrategy(MergeStrategy.MERGE_COMMIT);
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
request.setMessageTemplate("simple");
Assertions.assertThrows(UnexpectedMergeResultException.class, () -> command.merge(request));
}
@Test
public void shouldTakeAuthorFromSubjectIfNotSet() throws IOException, GitAPIException {
SimplePrincipalCollection principals = new SimplePrincipalCollection();
principals.add("admin", REALM);
principals.add( new User("dirk", "Dirk Gently", "dirk@holistic.det"), REALM);
principals.add(new User("dirk", "Dirk Gently", "dirk@holistic.det"), REALM);
shiro.setSubject(
new Subject.Builder()
.principals(principals)
@@ -364,6 +394,20 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
}
private GitMergeCommand createCommand() {
return new GitMergeCommand(createContext(), repository, new SimpleGitWorkdirFactory(new WorkdirProvider()));
return createCommand(git -> {
});
}
private GitMergeCommand createCommand(Consumer<Git> interceptor) {
return new GitMergeCommand(createContext(), repository, new SimpleGitWorkdirFactory(new WorkdirProvider())) {
@Override
<R, W extends GitCloneWorker<R>> R inClone(Function<Git, W> workerSupplier, GitWorkdirFactory workdirFactory, String initialBranch) {
Function<Git, W> interceptedWorkerSupplier = git -> {
interceptor.accept(git);
return workerSupplier.apply(git);
};
return super.inClone(interceptedWorkerSupplier, workdirFactory, initialBranch);
}
};
}
}

View File

@@ -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;

View File

@@ -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();

View File

@@ -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();

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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);

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -484,7 +484,7 @@ exports[`Storyshots DateFromNow Default 1`] = `
exports[`Storyshots Diff Binaries 1`] = `
<div
className="sc-TOsTZ flmUBf"
className="sc-hmzhuo TypKC"
>
<div
className="sc-gZMcBi iABzaT panel is-size-6"
@@ -753,7 +753,7 @@ exports[`Storyshots Diff Binaries 1`] = `
exports[`Storyshots Diff Collapsed 1`] = `
<div
className="sc-TOsTZ flmUBf"
className="sc-hmzhuo TypKC"
>
<div
className="sc-gZMcBi ckdmuY panel is-size-6"
@@ -1102,7 +1102,7 @@ exports[`Storyshots Diff Collapsed 1`] = `
exports[`Storyshots Diff CollapsingWithFunction 1`] = `
<div
className="sc-TOsTZ flmUBf"
className="sc-hmzhuo TypKC"
>
<div
className="sc-gZMcBi ckdmuY panel is-size-6"
@@ -3028,7 +3028,7 @@ exports[`Storyshots Diff CollapsingWithFunction 1`] = `
exports[`Storyshots Diff Default 1`] = `
<div
className="sc-TOsTZ flmUBf"
className="sc-hmzhuo TypKC"
>
<div
className="sc-gZMcBi iABzaT panel is-size-6"
@@ -6936,7 +6936,7 @@ exports[`Storyshots Diff Default 1`] = `
exports[`Storyshots Diff File Annotation 1`] = `
<div
className="sc-TOsTZ flmUBf"
className="sc-hmzhuo TypKC"
>
<div
className="sc-gZMcBi iABzaT panel is-size-6"
@@ -10868,7 +10868,7 @@ exports[`Storyshots Diff File Annotation 1`] = `
exports[`Storyshots Diff File Controls 1`] = `
<div
className="sc-TOsTZ flmUBf"
className="sc-hmzhuo TypKC"
>
<div
className="sc-gZMcBi iABzaT panel is-size-6"
@@ -14884,7 +14884,7 @@ exports[`Storyshots Diff File Controls 1`] = `
exports[`Storyshots Diff Hunks 1`] = `
<div
className="sc-TOsTZ flmUBf"
className="sc-hmzhuo TypKC"
>
<div
className="sc-gZMcBi iABzaT panel is-size-6"
@@ -15721,7 +15721,7 @@ exports[`Storyshots Diff Hunks 1`] = `
exports[`Storyshots Diff Line Annotation 1`] = `
<div
className="sc-TOsTZ flmUBf"
className="sc-hmzhuo TypKC"
>
<div
className="sc-gZMcBi iABzaT panel is-size-6"
@@ -19665,7 +19665,7 @@ exports[`Storyshots Diff Line Annotation 1`] = `
exports[`Storyshots Diff OnClick 1`] = `
<div
className="sc-TOsTZ flmUBf"
className="sc-hmzhuo TypKC"
>
<div
className="sc-gZMcBi iABzaT panel is-size-6"
@@ -23847,7 +23847,7 @@ exports[`Storyshots Diff OnClick 1`] = `
exports[`Storyshots Diff Side-By-Side 1`] = `
<div
className="sc-TOsTZ flmUBf"
className="sc-hmzhuo TypKC"
>
<div
className="sc-gZMcBi iABzaT panel is-size-6"
@@ -28284,7 +28284,7 @@ exports[`Storyshots Diff Side-By-Side 1`] = `
exports[`Storyshots Diff SyntaxHighlighting 1`] = `
<div
className="sc-TOsTZ flmUBf"
className="sc-hmzhuo TypKC"
>
<div
className="sc-gZMcBi iABzaT panel is-size-6"
@@ -32192,7 +32192,7 @@ exports[`Storyshots Diff SyntaxHighlighting 1`] = `
exports[`Storyshots Forms|Checkbox Default 1`] = `
<div
className="sc-gisBJw jHakbY"
className="sc-kgAjT khfRmZ"
>
<div
className="field"
@@ -32237,7 +32237,7 @@ exports[`Storyshots Forms|Checkbox Default 1`] = `
exports[`Storyshots Forms|Checkbox Disabled 1`] = `
<div
className="sc-gisBJw jHakbY"
className="sc-kgAjT khfRmZ"
>
<div
className="field"
@@ -32265,7 +32265,7 @@ exports[`Storyshots Forms|Checkbox Disabled 1`] = `
exports[`Storyshots Forms|Radio Default 1`] = `
<div
className="sc-kjoXOD hVPZau"
className="sc-cJSrbW hLoADP"
>
<label
className="sc-cMljjf kOqpHe radio"
@@ -32294,7 +32294,7 @@ exports[`Storyshots Forms|Radio Default 1`] = `
exports[`Storyshots Forms|Radio Disabled 1`] = `
<div
className="sc-kjoXOD hVPZau"
className="sc-cJSrbW hLoADP"
>
<label
className="sc-cMljjf kOqpHe radio"
@@ -32314,7 +32314,7 @@ exports[`Storyshots Forms|Radio Disabled 1`] = `
exports[`Storyshots Forms|Textarea OnCancel 1`] = `
<div
className="sc-cHGsZl klfJMr"
className="sc-ksYbfQ ePXdiL"
>
<div
className="field"
@@ -32337,7 +32337,7 @@ exports[`Storyshots Forms|Textarea OnCancel 1`] = `
exports[`Storyshots Forms|Textarea OnChange 1`] = `
<div
className="sc-cHGsZl klfJMr"
className="sc-ksYbfQ ePXdiL"
>
<div
className="field"
@@ -32364,7 +32364,7 @@ exports[`Storyshots Forms|Textarea OnChange 1`] = `
exports[`Storyshots Forms|Textarea OnSubmit 1`] = `
<div
className="sc-cHGsZl klfJMr"
className="sc-ksYbfQ ePXdiL"
>
<div
className="field"
@@ -32389,6 +32389,514 @@ exports[`Storyshots Forms|Textarea OnSubmit 1`] = `
</div>
`;
exports[`Storyshots Layout|Footer Default 1`] = `
<footer
className="footer"
>
<section
className="section container"
>
<div
className="columns is-size-7"
>
<section
className="column is-one-third"
>
<div
className="sc-hzDkRC jeksqW"
>
<i
className="fas fa-user-circle fa-fw"
/>
Trillian McMillian
</div>
<ul
className="sc-jhAzac eoWThz"
>
<li>
<a
className=""
href="/me"
onClick={[Function]}
>
footer.user.profile
</a>
</li>
<li>
<a
className=""
href="/me/settings/password"
onClick={[Function]}
>
profile.changePasswordNavLink
</a>
</li>
</ul>
</section>
<section
className="column is-one-third"
>
<div
className="sc-hzDkRC jeksqW"
>
<i
className="fas fa-info-circle fa-fw"
/>
footer.information.title
</div>
<ul
className="sc-jhAzac eoWThz"
>
<li>
<a
href="https://www.scm-manager.org/"
target="_blank"
>
SCM-Manager 2.0.0
</a>
</li>
</ul>
</section>
<section
className="column is-one-third"
>
<div
className="sc-hzDkRC jeksqW"
>
<i
className="fas fa-life-ring fa-fw"
/>
footer.support.title
</div>
<ul
className="sc-jhAzac eoWThz"
>
<li>
<a
href="https://www.scm-manager.org/support/"
target="_blank"
>
footer.support.community
</a>
</li>
<li>
<a
href="https://cloudogu.com/en/scm-manager-enterprise/"
target="_blank"
>
footer.support.enterprise
</a>
</li>
</ul>
</section>
</div>
</section>
</footer>
`;
exports[`Storyshots Layout|Footer Full 1`] = `
<footer
className="footer"
>
<section
className="section container"
>
<div
className="columns is-size-7"
>
<section
className="column is-one-third"
>
<div
className="sc-hzDkRC jeksqW"
>
<span
className="sc-fMiknA fyPpQQ image is-rounded"
>
<img
alt="trillian"
className="is-rounded sc-fBuWsC djJrAv"
src="test-file-stub"
/>
</span>
Trillian McMillian
</div>
<ul
className="sc-jhAzac eoWThz"
>
<li>
<a
className=""
href="/me"
onClick={[Function]}
>
footer.user.profile
</a>
</li>
<li>
<a
className=""
href="/me/settings/password"
onClick={[Function]}
>
profile.changePasswordNavLink
</a>
</li>
<li>
<a
className=""
href="/"
onClick={[Function]}
>
Authorized Keys
</a>
</li>
</ul>
</section>
<section
className="column is-one-third"
>
<div
className="sc-hzDkRC jeksqW"
>
<i
className="fas fa-info-circle fa-fw"
/>
footer.information.title
</div>
<ul
className="sc-jhAzac eoWThz"
>
<li>
<a
href="https://www.scm-manager.org/"
target="_blank"
>
SCM-Manager 2.0.0
</a>
</li>
<li>
<a
href="#"
target="_blank"
>
REST API
</a>
</li>
<li>
<a
href="#"
target="_blank"
>
CLI
</a>
</li>
</ul>
</section>
<section
className="column is-one-third"
>
<div
className="sc-hzDkRC jeksqW"
>
<i
className="fas fa-life-ring fa-fw"
/>
footer.support.title
</div>
<ul
className="sc-jhAzac eoWThz"
>
<li>
<a
href="https://www.scm-manager.org/support/"
target="_blank"
>
footer.support.community
</a>
</li>
<li>
<a
href="https://cloudogu.com/en/scm-manager-enterprise/"
target="_blank"
>
footer.support.enterprise
</a>
</li>
<li>
<a
href="#"
target="_blank"
>
FAQ
</a>
</li>
</ul>
</section>
</div>
</section>
</footer>
`;
exports[`Storyshots Layout|Footer With Avatar 1`] = `
<footer
className="footer"
>
<section
className="section container"
>
<div
className="columns is-size-7"
>
<section
className="column is-one-third"
>
<div
className="sc-hzDkRC jeksqW"
>
<span
className="sc-fMiknA fyPpQQ image is-rounded"
>
<img
alt="trillian"
className="is-rounded sc-fBuWsC djJrAv"
src="test-file-stub"
/>
</span>
Trillian McMillian
</div>
<ul
className="sc-jhAzac eoWThz"
>
<li>
<a
className=""
href="/me"
onClick={[Function]}
>
footer.user.profile
</a>
</li>
<li>
<a
className=""
href="/me/settings/password"
onClick={[Function]}
>
profile.changePasswordNavLink
</a>
</li>
</ul>
</section>
<section
className="column is-one-third"
>
<div
className="sc-hzDkRC jeksqW"
>
<i
className="fas fa-info-circle fa-fw"
/>
footer.information.title
</div>
<ul
className="sc-jhAzac eoWThz"
>
<li>
<a
href="https://www.scm-manager.org/"
target="_blank"
>
SCM-Manager 2.0.0
</a>
</li>
</ul>
</section>
<section
className="column is-one-third"
>
<div
className="sc-hzDkRC jeksqW"
>
<i
className="fas fa-life-ring fa-fw"
/>
footer.support.title
</div>
<ul
className="sc-jhAzac eoWThz"
>
<li>
<a
href="https://www.scm-manager.org/support/"
target="_blank"
>
footer.support.community
</a>
</li>
<li>
<a
href="https://cloudogu.com/en/scm-manager-enterprise/"
target="_blank"
>
footer.support.enterprise
</a>
</li>
</ul>
</section>
</div>
</section>
</footer>
`;
exports[`Storyshots Layout|Footer With Plugin Links 1`] = `
<footer
className="footer"
>
<section
className="section container"
>
<div
className="columns is-size-7"
>
<section
className="column is-one-third"
>
<div
className="sc-hzDkRC jeksqW"
>
<i
className="fas fa-user-circle fa-fw"
/>
Trillian McMillian
</div>
<ul
className="sc-jhAzac eoWThz"
>
<li>
<a
className=""
href="/me"
onClick={[Function]}
>
footer.user.profile
</a>
</li>
<li>
<a
className=""
href="/me/settings/password"
onClick={[Function]}
>
profile.changePasswordNavLink
</a>
</li>
<li>
<a
className=""
href="/"
onClick={[Function]}
>
Authorized Keys
</a>
</li>
</ul>
</section>
<section
className="column is-one-third"
>
<div
className="sc-hzDkRC jeksqW"
>
<i
className="fas fa-info-circle fa-fw"
/>
footer.information.title
</div>
<ul
className="sc-jhAzac eoWThz"
>
<li>
<a
href="https://www.scm-manager.org/"
target="_blank"
>
SCM-Manager 2.0.0
</a>
</li>
<li>
<a
href="#"
target="_blank"
>
REST API
</a>
</li>
<li>
<a
href="#"
target="_blank"
>
CLI
</a>
</li>
</ul>
</section>
<section
className="column is-one-third"
>
<div
className="sc-hzDkRC jeksqW"
>
<i
className="fas fa-life-ring fa-fw"
/>
footer.support.title
</div>
<ul
className="sc-jhAzac eoWThz"
>
<li>
<a
href="https://www.scm-manager.org/support/"
target="_blank"
>
footer.support.community
</a>
</li>
<li>
<a
href="https://cloudogu.com/en/scm-manager-enterprise/"
target="_blank"
>
footer.support.enterprise
</a>
</li>
<li>
<a
href="#"
target="_blank"
>
FAQ
</a>
</li>
</ul>
</section>
</div>
</section>
</footer>
`;
exports[`Storyshots Loading Default 1`] = `
<div>
<div
@@ -34243,7 +34751,7 @@ PORT_NUMBER =
exports[`Storyshots Table|Table Default 1`] = `
<table
className="sc-fBuWsC eeihxG table content is-hoverable"
className="sc-fAjcbJ byigni table content is-hoverable"
>
<thead>
<tr>
@@ -34261,7 +34769,7 @@ exports[`Storyshots Table|Table Default 1`] = `
>
Last Name
<i
className="fas fa-sort-amount-down has-text-grey-light sc-jhAzac gDbcZp"
className="fas fa-sort-amount-down has-text-grey-light sc-eqIVtm jxAoDg"
/>
</th>
<th
@@ -34334,7 +34842,7 @@ exports[`Storyshots Table|Table Empty 1`] = `
exports[`Storyshots Table|Table TextColumn 1`] = `
<table
className="sc-fBuWsC eeihxG table content is-hoverable"
className="sc-fAjcbJ byigni table content is-hoverable"
>
<thead>
<tr>
@@ -34346,7 +34854,7 @@ exports[`Storyshots Table|Table TextColumn 1`] = `
>
Id
<i
className="fas fa-sort-alpha-down has-text-grey-light sc-jhAzac gDbcZp"
className="fas fa-sort-alpha-down has-text-grey-light sc-eqIVtm jxAoDg"
/>
</th>
<th
@@ -34357,7 +34865,7 @@ exports[`Storyshots Table|Table TextColumn 1`] = `
>
Name
<i
className="fas fa-sort-alpha-down has-text-grey-light sc-jhAzac gDbcZp"
className="fas fa-sort-alpha-down has-text-grey-light sc-eqIVtm jxAoDg"
/>
</th>
<th
@@ -34368,7 +34876,7 @@ exports[`Storyshots Table|Table TextColumn 1`] = `
>
Description
<i
className="fas fa-sort-alpha-down has-text-grey-light sc-jhAzac gDbcZp"
className="fas fa-sort-alpha-down has-text-grey-light sc-eqIVtm jxAoDg"
/>
</th>
</tr>

View File

@@ -1,26 +1,30 @@
import React 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";
className?: string;
};
class AvatarImage extends React.Component<Props> {
render() {
const { person } = this.props;
const AvatarImage: FC<Props> = ({ person, representation = "rounded-border", className }) => {
const binder = useBinder();
const avatarFactory = binder.getExtension(EXTENSION_POINT);
if (avatarFactory) {
const avatar = avatarFactory(person);
const avatarFactory = binder.getExtension(EXTENSION_POINT);
if (avatarFactory) {
const avatar = avatarFactory(person);
return <Image className="has-rounded-border" src={avatar} alt={person.name} />;
let classes = representation === "rounded" ? "is-rounded" : "has-rounded-border";
if (className) {
classes += " " + className;
}
return null;
return <Image className={classes} src={avatar} alt={person.name} />;
}
}
return null;
};
export default AvatarImage;

View File

@@ -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<Props> {
render() {
if (binder.hasExtension(EXTENSION_POINT)) {
return <>{this.props.children}</>;
}
return null;
}
}
export default AvatarWrapper;

View File

@@ -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]) {

View File

@@ -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) => {

View File

@@ -0,0 +1,62 @@
import React from "react";
import { storiesOf } from "@storybook/react";
import Footer from "./Footer";
import { Binder, BinderContext } from "@scm-manager/ui-extensions";
import { Me } from "@scm-manager/ui-types";
import { EXTENSION_POINT } from "../avatar/Avatar";
// @ts-ignore ignore unknown png
import hitchhiker from "../__resources__/hitchhiker.png";
// @ts-ignore ignore unknown jpg
import marvin from "../__resources__/marvin.jpg";
import NavLink from "../navigation/NavLink";
import ExternalLink from "../navigation/ExternalLink";
const trillian: Me = {
name: "trillian",
displayName: "Trillian McMillian",
mail: "tricia@hitchhiker.com",
groups: ["crew"],
_links: {}
};
const bindAvatar = (binder: Binder, avatar: string) => {
binder.bind(EXTENSION_POINT, () => {
return avatar;
});
};
const bindLinks = (binder: Binder) => {
binder.bind("footer.information", () => <ExternalLink to="#" label="REST API" />);
binder.bind("footer.information", () => <ExternalLink to="#" label="CLI" />);
binder.bind("footer.support", () => <ExternalLink to="#" label="FAQ" />);
binder.bind("profile.setting", () => <NavLink label="Authorized Keys" to="#" />);
};
const withBinder = (binder: Binder) => {
return (
<BinderContext.Provider value={binder}>
<Footer me={trillian} version="2.0.0" links={{}} />
</BinderContext.Provider>
);
};
storiesOf("Layout|Footer", module)
.add("Default", () => {
return <Footer me={trillian} version="2.0.0" links={{}} />;
})
.add("With Avatar", () => {
const binder = new Binder("avatar-story");
bindAvatar(binder, hitchhiker);
return withBinder(binder);
})
.add("With Plugin Links", () => {
const binder = new Binder("link-story");
bindLinks(binder);
return withBinder(binder);
})
.add("Full", () => {
const binder = new Binder("link-story");
bindAvatar(binder, marvin);
bindLinks(binder);
return withBinder(binder);
});

View File

@@ -1,27 +1,93 @@
import React from "react";
import { Me } from "@scm-manager/ui-types";
import { Link } from "react-router-dom";
import React, { FC } from "react";
import { Me, Links } from "@scm-manager/ui-types";
import { useBinder, ExtensionPoint } from "@scm-manager/ui-extensions";
import { AvatarImage } from "../avatar";
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;
version: string;
links: Links;
};
class Footer extends React.Component<Props> {
render() {
const { me } = this.props;
if (!me) {
return "";
}
return (
<footer className="footer">
<div className="container is-centered">
<p className="has-text-centered">
<Link to={"/me"}>{me.displayName}</Link>
</p>
</div>
</footer>
);
type TitleWithIconsProps = {
title: string;
icon: string;
};
const TitleWithIcon: FC<TitleWithIconsProps> = ({ icon, title }) => (
<>
<i className={`fas fa-${icon} fa-fw`} /> {title}
</>
);
type TitleWithAvatarProps = {
me: Me;
};
const VCenteredAvatar = styled(AvatarImage)`
vertical-align: middle;
`;
const AvatarContainer = styled.span`
float: left;
margin-right: 0.3em;
padding-top: 0.2em;
width: 1em;
height: 1em;
`;
const TitleWithAvatar: FC<TitleWithAvatarProps> = ({ me }) => (
<>
<AvatarContainer className="image is-rounded">
<VCenteredAvatar person={me} representation="rounded" />
</AvatarContainer>
{me.displayName}
</>
);
const Footer: FC<Props> = ({ 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)) {
meSectionTile = <TitleWithAvatar me={me} />;
} else {
meSectionTile = <TitleWithIcon title={me.displayName} icon="user-circle" />;
}
return (
<footer className="footer">
<section className="section container">
<div className="columns is-size-7">
<FooterSection title={meSectionTile}>
<NavLink to="/me" label={t("footer.user.profile")} />
<NavLink to="/me/settings/password" label={t("profile.changePasswordNavLink")} />
<ExtensionPoint name="profile.setting" props={extensionProps} renderAll={true} />
</FooterSection>
<FooterSection title={<TitleWithIcon title={t("footer.information.title")} icon="info-circle" />}>
<ExternalLink to="https://www.scm-manager.org/" label={`SCM-Manager ${version}`} />
<ExtensionPoint name="footer.information" props={extensionProps} renderAll={true} />
</FooterSection>
<FooterSection title={<TitleWithIcon title={t("footer.support.title")} icon="life-ring" />}>
<ExternalLink to="https://www.scm-manager.org/support/" label={t("footer.support.community")} />
<ExternalLink to="https://cloudogu.com/en/scm-manager-enterprise/" label={t("footer.support.enterprise")} />
<ExtensionPoint name="footer.support" props={extensionProps} renderAll={true} />
</FooterSection>
</div>
</section>
</footer>
);
};
export default Footer;

View File

@@ -0,0 +1,26 @@
import React, { FC, ReactNode } from "react";
import styled from "styled-components";
type Props = {
title: ReactNode;
};
const Title = styled.div`
font-weight: bold;
margin-bottom: 0.5rem;
`;
const Menu = styled.ul`
padding-left: 1.1rem;
`;
const FooterSection: FC<Props> = ({ title, children }) => {
return (
<section className="column is-one-third">
<Title>{title}</Title>
<Menu>{children}</Menu>
</section>
);
};
export default FooterSection;

View File

@@ -0,0 +1,30 @@
import React, { FC } from "react";
import classNames from "classnames";
type Props = {
to: string;
icon?: string;
label: string;
};
const ExternalLink: FC<Props> = ({ to, icon, label }) => {
let showIcon;
if (icon) {
showIcon = (
<>
<i className={classNames(icon, "fa-fw")} />{" "}
</>
);
}
return (
<li>
<a target="_blank" href={to}>
{showIcon}
{label}
</a>
</li>
);
};
export default ExternalLink;

View File

@@ -33,12 +33,6 @@ describe("ExtensionPoint test", () => {
expect(rendered.text()).toBe("Extension One");
});
// We use this wrapper since Enzyme cannot handle React Fragments (see https://github.com/airbnb/enzyme/issues/1213)
class ExtensionPointEnzymeFix extends ExtensionPoint {
render() {
return <div>{super.render()}</div>;
}
}
it("should render the given components", () => {
const labelOne = () => {
return <label>Extension One</label>;
@@ -50,7 +44,7 @@ describe("ExtensionPoint test", () => {
mockedBinder.hasExtension.mockReturnValue(true);
mockedBinder.getExtensions.mockReturnValue([labelOne, labelTwo]);
const rendered = mount(<ExtensionPointEnzymeFix name="something.special" renderAll={true} />);
const rendered = mount(<ExtensionPoint name="something.special" renderAll={true} />);
const text = rendered.text();
expect(text).toContain("Extension One");
expect(text).toContain("Extension Two");
@@ -143,4 +137,12 @@ describe("ExtensionPoint test", () => {
const text = rendered.text();
expect(text).toBe("Hello Trillian");
});
it("should not render nothing without extension and without default", () => {
mockedBinder.hasExtension.mockReturnValue(false);
const rendered = mount(<ExtensionPoint name="something.special" />);
const text = rendered.text();
expect(text).toBe("");
});
});

View File

@@ -1,53 +1,51 @@
import * as React from "react";
import binder from "./binder";
import { Binder } from "./binder";
import { FC, ReactNode } from "react";
import useBinder from "./useBinder";
type Props = {
name: string;
renderAll?: boolean;
props?: object;
children?: React.ReactNode;
};
const renderAllExtensions = (binder: Binder, name: string, props?: object) => {
const extensions = binder.getExtensions(name, props);
return (
<>
{extensions.map((Component, index) => {
return <Component key={index} {...props} />;
})}
</>
);
};
const renderSingleExtension = (binder: Binder, name: string, props?: object) => {
const Component = binder.getExtension(name, props);
if (!Component) {
return null;
}
return <Component {...props} />;
};
const renderDefault = (children: ReactNode) => {
if (children) {
return <>{children}</>;
}
return null;
};
/**
* ExtensionPoint renders components which are bound to an extension point.
*/
class ExtensionPoint extends React.Component<Props> {
renderAll(name: string, props?: object) {
const extensions = binder.getExtensions(name, props);
return (
<>
{extensions.map((Component, index) => {
return <Component key={index} {...props} />;
})}
</>
);
const ExtensionPoint: FC<Props> = ({ name, renderAll, props, children }) => {
const binder = useBinder();
if (!binder.hasExtension(name, props)) {
return renderDefault(children);
} else if (renderAll) {
return renderAllExtensions(binder, name, props);
}
renderSingle(name: string, props?: object) {
const Component = binder.getExtension(name, props);
if (!Component) {
return null;
}
return <Component {...props} />;
}
renderDefault() {
const { children } = this.props;
if (children) {
return <>{children}</>;
}
return null;
}
render() {
const { name, renderAll, props } = this.props;
if (!binder.hasExtension(name, props)) {
return this.renderDefault();
} else if (renderAll) {
return this.renderAll(name, props);
}
return this.renderSingle(name, props);
}
}
return renderSingleExtension(binder, name, props);
};
export default ExtensionPoint;

View File

@@ -4,7 +4,7 @@ describe("binder tests", () => {
let binder: Binder;
beforeEach(() => {
binder = new Binder();
binder = new Binder("testing");
});
it("should return an empty array for non existing extension points", () => {
@@ -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");
});
});

View File

@@ -3,6 +3,7 @@ type Predicate = (props: any) => boolean;
type ExtensionRegistration = {
predicate: Predicate;
extension: any;
extensionName: string;
};
/**
@@ -10,11 +11,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<ExtensionRegistration>;
};
constructor() {
constructor(name: string) {
this.name = name;
this.extensionPoints = {};
}
@@ -25,13 +28,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 +65,7 @@ export class Binder {
if (props) {
registrations = registrations.filter(reg => reg.predicate(props || {}));
}
registrations.sort(this.sortExtensions);
return registrations.map(reg => reg.extension);
}
@@ -70,9 +75,28 @@ export class Binder {
hasExtension(extensionPoint: string, props?: object): boolean {
return this.getExtensions(extensionPoint, props).length > 0;
}
/**
* Sort extensions in ascending order, starting with entries with specified extensionName.
*/
sortExtensions = (a: ExtensionRegistration, b: ExtensionRegistration) => {
const regA = a.extensionName ? a.extensionName.toUpperCase() : "";
const regB = b.extensionName ? b.extensionName.toUpperCase() : "";
if (regA === "" && regB !== "") {
return 1;
} else if (regA !== "" && regB === "") {
return -1;
} else if (regA > regB) {
return 1;
} else if (regA < regB) {
return -1;
}
return 0;
};
}
// singleton binder
const binder = new Binder();
const binder = new Binder("default");
export default binder;

View File

@@ -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";

View File

@@ -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(<BinderName />);
expect(rendered.text()).toBe("default");
});
it("should return binder from context", () => {
const binder = new Binder("from-context");
const app = (
<BinderContext.Provider value={binder}>
<BinderName />
</BinderContext.Provider>
);
const rendered = mount(app);
expect(rendered.text()).toBe("from-context");
});
});

View File

@@ -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;

View File

@@ -67,8 +67,14 @@ hr.header-with-actions {
}
}
.footer {
height: 50px;
footer.footer {
//height: 100px;
background-color: $white-ter;
padding: inherit;
a {
color: darken($blue, 15%);
}
}
// 6. Import the rest of Bulma
@@ -691,11 +697,6 @@ form .field:not(.is-grouped) {
}
}
// footer
.footer {
background-color: whitesmoke;
}
// aside
.aside-background {
bottom: 0;

View File

@@ -4,6 +4,6 @@ export type Me = {
name: string;
displayName: string;
mail: string;
groups: [];
groups: string[];
_links: Links;
};

View File

@@ -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"
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -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<Props> {
}
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 ? <PrimaryNavigation links={links} /> : "";
@@ -50,7 +52,7 @@ class App extends Component<Props> {
<div className="App">
<Header>{navigation}</Header>
{content}
{authenticated && <Footer me={me} />}
{authenticated && <Footer me={me} version={version} links={links} />}
</div>
);
}
@@ -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
};
};

View File

@@ -426,6 +426,13 @@
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<version>2.1.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
@@ -471,6 +478,49 @@
</executions>
</plugin>
<plugin>
<groupId>io.openapitools.swagger</groupId>
<artifactId>swagger-maven-plugin</artifactId>
<configuration>
<resourcePackages>
<resourcePackage>sonia.scm.api.v2.resources</resourcePackage>
</resourcePackages>
<outputDirectory>${basedir}/target/openapi/META-INF/scm</outputDirectory>
<outputFilename>openapi</outputFilename>
<outputFormats>JSON,YAML</outputFormats>
<prettyPrint>true</prettyPrint>
<swaggerConfig>
<servers>
<server>
<url>http://localhost:8081/scm/api</url>
<description>local endpoint url</description>
</server>
</servers>
<info>
<title>SCM-Manager REST-API</title>
<version>${project.version}</version>
<contact>
<email>scmmanager@googlegroups.com</email>
<name>SCM-Manager</name>
<url>https://scm-manager.org</url>
</contact>
<license>
<url>http://www.opensource.org/licenses/bsd-license.php</url>
<name>BSD</name>
</license>
</info>
<descriptionFile>src/main/doc/openapi.md</descriptionFile>
</swaggerConfig>
</configuration>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>sonia.scm.maven</groupId>
<artifactId>smp-maven-plugin</artifactId>
@@ -511,9 +561,15 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.2</version>
<version>3.1.0</version>
<configuration>
<filteringDeploymentDescriptors>true</filteringDeploymentDescriptors>
<webResources>
<resource>
<directory>target/openapi</directory>
<targetPath>WEB-INF/classes</targetPath>
</resource>
</webResources>
</configuration>
</plugin>
@@ -860,107 +916,9 @@
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>doc</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>copy-enunciate-configuration</id>
<phase>compile</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}</outputDirectory>
<resources>
<resource>
<directory>src/main/doc</directory>
<filtering>true</filtering>
<includes>
<include>**/enunciate.xml</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.webcohesion.enunciate</groupId>
<artifactId>enunciate-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>docs</goal>
</goals>
<phase>compile</phase>
</execution>
</executions>
<configuration>
<configFile>${project.build.directory}/enunciate.xml</configFile>
<docsDir>${project.build.directory}</docsDir>
<docsSubdir>restdocs</docsSubdir>
</configuration>
<dependencies>
<dependency>
<groupId>com.webcohesion.enunciate</groupId>
<artifactId>enunciate-top</artifactId>
<version>${enunciate.version}</version>
<exclusions>
<exclusion>
<groupId>com.webcohesion.enunciate</groupId>
<artifactId>enunciate-swagger</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.webcohesion.enunciate</groupId>
<artifactId>enunciate-lombok</artifactId>
<version>${enunciate.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptors>
<descriptor>src/main/doc/assembly.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View File

@@ -1,67 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
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.
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.
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
DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
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
-->
<!--
Document : enunciate.xml
Created on : October 2, 2011, 12:02 PM
Author : Sebastian Sdorra
Description: Enunciate configuration
-->
<enunciate xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://enunciate.webcohesion.com/schemas/enunciate-2.9.0.xsd"
slug="scm-manager" version="${project.version}">
<title>SCM-Manager API</title>
<description>
<![CDATA[
<h1>SCM-Manager API</h1>
<p>This page describes the RESTful Web Service API of <a href="https://www.scm-manager.org">SCM-Manager</a> ${project.version}.</p>
]]>
</description>
<api-classes/>
<modules>
<jaxrs datatype-detection="local">
<application path="/api" />
</jaxrs>
<docs disableResourceLinks="true" includeApplicationPath="true" />
</modules>
</enunciate>

View File

@@ -0,0 +1,15 @@
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 login to the SCM-Manager. You can also 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 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 HAL links when creating new functions for SCM-Manager since they are consistent and are only
appended to the response when user has the necessary permissions. The links and embedded resources can also be used by plugins, which can
define new resources or enrich existing ones.

View File

@@ -1,19 +1,19 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* <p>
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* <p>
* 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.
* <p>
* 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.
*
* <p>
* 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}. <strong>Note:</strong> 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. <strong>Note:</strong> 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. <strong>Note:</strong> 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. <strong>Note:</strong> 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<Repository> repositories = new ArrayList<Repository>();
@@ -315,7 +248,8 @@ public class RepositoryImportResource
//J-
return Response.ok(
new GenericEntity<List<Repository>>(repositories) {}
new GenericEntity<List<Repository>>(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<Repository> repositories = new ArrayList<Repository>();
for (Type t : findImportableTypes())
{
for (Type t : findImportableTypes()) {
importFromDirectory(repositories, t.getName());
}
//J-
return Response.ok(
new GenericEntity<List<Repository>>(repositories) {}
new GenericEntity<List<Repository>>(repositories) {
}
).build();
//J+
}
@@ -363,72 +287,50 @@ public class RepositoryImportResource
* of failed directories. <strong>Note:</strong> 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.<String>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<Type> types = findImportableTypes();
//J-
return Response.ok(
new GenericEntity<List<Type>>(types) {}
new GenericEntity<List<Type>>(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<Command> 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<Type> findImportableTypes()
{
private List<Type> findImportableTypes() {
List<Type> types = new ArrayList<Type>();
Collection<Type> 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<Repository> repositories, String type)
{
private void importFromDirectory(List<Repository> 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<String> 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;
}

View File

@@ -1,24 +1,63 @@
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.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.Operation;
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;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.security.*;
import sonia.scm.security.AccessToken;
import sonia.scm.security.AccessTokenBuilder;
import sonia.scm.security.AccessTokenBuilderFactory;
import sonia.scm.security.AccessTokenCookieIssuer;
import sonia.scm.security.AllowAnonymousAccess;
import sonia.scm.security.Scope;
import sonia.scm.security.Tokens;
import sonia.scm.web.VndMediaType;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.*;
import javax.ws.rs.BeanParam;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
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",
description = "Authentication with a jwt bearer token",
scheme = "bearer",
bearerFormat = "JWT",
type = SecuritySchemeType.HTTP
)
})
@OpenAPIDefinition(tags = {
@Tag(name = "Authentication", description = "Authentication related endpoints")
})
@Path(AuthenticationResource.PATH)
@AllowAnonymousAccess
public class AuthenticationResource {
@@ -34,21 +73,33 @@ public class AuthenticationResource {
private LogoutRedirection logoutRedirection;
@Inject
public AuthenticationResource(AccessTokenBuilderFactory tokenBuilderFactory, AccessTokenCookieIssuer cookieIssuer)
{
public AuthenticationResource(AccessTokenBuilderFactory tokenBuilderFactory, AccessTokenCookieIssuer cookieIssuer) {
this.tokenBuilderFactory = tokenBuilderFactory;
this.cookieIssuer = cookieIssuer;
}
@POST
@Path("access_token")
@Produces(MediaType.TEXT_PLAIN)
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 400, condition = "bad request, required parameter is missing"),
@ResponseCode(code = 401, condition = "unauthorized, the specified username or password is wrong"),
@ResponseCode(code = 500, condition = "internal server error")
})
@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(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
public Response authenticateViaForm(
@Context HttpServletRequest request,
@Context HttpServletResponse response,
@@ -59,18 +110,41 @@ public class AuthenticationResource {
@POST
@Path("access_token")
@Produces(MediaType.TEXT_PLAIN)
@Consumes(MediaType.APPLICATION_JSON)
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 400, condition = "bad request, required parameter is missing"),
@ResponseCode(code = 401, condition = "unauthorized, the specified username or password is wrong"),
@ResponseCode(code = 500, condition = "internal server error")
})
@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(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
public Response authenticateViaJSONBody(
@Context HttpServletRequest request,
@Context HttpServletResponse response,
AuthenticationRequestDto authentication
) {
) {
return authenticate(request, response, authentication);
}
@@ -86,12 +160,11 @@ public class AuthenticationResource {
Response res;
Subject subject = SecurityUtils.getSubject();
try
{
try {
subject.login(Tokens.createAuthenticationToken(request, authentication.getUsername(), authentication.getPassword()));
AccessTokenBuilder tokenBuilder = tokenBuilderFactory.create();
if ( authentication.getScope() != null ) {
if (authentication.getScope() != null) {
tokenBuilder.scope(Scope.valueOf(authentication.getScope()));
}
@@ -101,17 +174,12 @@ public class AuthenticationResource {
cookieIssuer.authenticate(request, response, token);
res = Response.noContent().build();
} else {
res = Response.ok( token.compact() ).build();
res = Response.ok(token.compact()).build();
}
}
catch (AuthenticationException ex)
{
if (LOG.isTraceEnabled())
{
} catch (AuthenticationException ex) {
if (LOG.isTraceEnabled()) {
LOG.trace("authentication failed for user ".concat(authentication.getUsername()), ex);
}
else
{
} else {
LOG.warn("authentication failed for user {}", authentication.getUsername());
}
@@ -126,12 +194,10 @@ public class AuthenticationResource {
@DELETE
@Path("access_token")
@Produces(MediaType.APPLICATION_JSON)
@StatusCodes({
@ResponseCode(code = 204, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
public Response logout(@Context HttpServletRequest request, @Context HttpServletResponse response)
{
@Operation(summary = "Logout", description = "Removes the access token.", tags = "Authentication")
@ApiResponse(responseCode = "204", description = "success")
@ApiResponse(responseCode = "500", description = "internal server error")
public Response logout(@Context HttpServletRequest request, @Context HttpServletResponse response) {
Subject subject = SecurityUtils.getSubject();
subject.logout();
@@ -139,7 +205,6 @@ public class AuthenticationResource {
// remove authentication cookie
cookieIssuer.invalidate(request, response);
// TODO anonymous access ??
if (logoutRedirection == null) {
return Response.noContent().build();
} else {

View File

@@ -1,14 +1,18 @@
package sonia.scm.api.v2.resources;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import javax.validation.constraints.NotEmpty;
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.ReducedModelObject;
import sonia.scm.group.GroupDisplayManager;
import sonia.scm.user.UserDisplayManager;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
@@ -18,7 +22,9 @@ import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@OpenAPIDefinition(tags = {
@Tag(name = "Autocomplete", description = "Autocomplete related endpoints")
})
@Path(AutoCompleteResource.PATH)
public class AutoCompleteResource {
public static final String PATH = "v2/autocomplete/";
@@ -43,13 +49,26 @@ public class AutoCompleteResource {
@GET
@Path("users")
@Produces(VndMediaType.AUTOCOMPLETE)
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 400, condition = "if the searched string contains less than 2 characters"),
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"user:autocomplete\" privilege"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Operation(summary = "Search user", description = "Returns matching users.", tags = "Autocomplete")
@ApiResponse(
responseCode = "200",
description = "success",
content = @Content(
mediaType = VndMediaType.AUTOCOMPLETE,
schema = @Schema(implementation = ReducedObjectModelDto.class)
)
)
@ApiResponse(responseCode = "400", description = "if the searched string contains less than 2 characters")
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"user:autocomplete\" privilege")
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
public List<ReducedObjectModelDto> searchUser(@NotEmpty(message = PARAMETER_IS_REQUIRED) @Size(min = MIN_SEARCHED_CHARS, message = INVALID_PARAMETER_LENGTH) @QueryParam("q") String filter) {
return map(userDisplayManager.autocomplete(filter));
}
@@ -57,13 +76,25 @@ public class AutoCompleteResource {
@GET
@Path("groups")
@Produces(VndMediaType.AUTOCOMPLETE)
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 400, condition = "if the searched string contains less than 2 characters"),
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"group:autocomplete\" privilege"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Operation(summary = "Search groups", description = "Returns matching groups.", tags = "Autocomplete")
@ApiResponse(
responseCode = "200",
description = "success",
content = @Content(
mediaType = VndMediaType.AUTOCOMPLETE,
schema = @Schema(implementation = ReducedObjectModelDto.class)
))
@ApiResponse(responseCode = "400", description = "if the searched string contains less than 2 characters")
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"group:autocomplete\" privilege")
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
public List<ReducedObjectModelDto> searchGroup(@NotEmpty(message = PARAMETER_IS_REQUIRED) @Size(min = MIN_SEARCHED_CHARS, message = INVALID_PARAMETER_LENGTH) @QueryParam("q") String filter) {
return map(groupDisplayManager.autocomplete(filter));
}

View File

@@ -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 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.plugin.AvailablePlugin;
import sonia.scm.plugin.InstalledPlugin;
import sonia.scm.plugin.PluginManager;
@@ -44,11 +46,29 @@ public class AvailablePluginResource {
*/
@GET
@Path("")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(CollectionDto.class)
@Operation(
summary = "Find all available plugins",
description = "Returns a collection of available plugins.",
tags = "Plugin Management"
)
@ApiResponse(
responseCode = "200",
description = "success",
content = @Content(
mediaType = VndMediaType.PLUGIN_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 \"plugin:read\" privilege")
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
@Produces(VndMediaType.PLUGIN_COLLECTION)
public Response getAvailablePlugins() {
PluginPermissions.read().check();
@@ -68,13 +88,37 @@ public class AvailablePluginResource {
*/
@GET
@Path("/{name}")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 404, condition = "not found"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(PluginDto.class)
@Produces(VndMediaType.PLUGIN)
@Operation(
summary = "Find single available plugin",
description = "Returns an available plugins.",
tags = "Plugin Management"
)
@ApiResponse(
responseCode = "200",
description = "success",
content = @Content(
mediaType = VndMediaType.PLUGIN,
schema = @Schema(implementation = PluginDto.class)
)
)
@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, 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",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
public Response getAvailablePlugin(@PathParam("name") String name) {
PluginPermissions.read().check();
Optional<AvailablePlugin> plugin = pluginManager.getAvailable(name);
@@ -87,15 +131,28 @@ public class AvailablePluginResource {
/**
* Triggers plugin installation.
*
* @param name plugin name
* @return HTTP Status.
*/
@POST
@Path("/{name}/install")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Operation(
summary = "Triggers plugin installation",
description = "Put single plugin in installation queue. Plugin will be installed after restart.",
tags = "Plugin Management"
)
@ApiResponse(responseCode = "200", description = "success")
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:manage\" privilege")
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
public Response installPlugin(@PathParam("name") String name, @QueryParam("restart") boolean restartAfterInstallation) {
PluginPermissions.manage().check();
pluginManager.install(name, restartAfterInstallation);

View File

@@ -1,11 +1,11 @@
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.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 sonia.scm.PageResult;
import sonia.scm.repository.Branch;
import sonia.scm.repository.Branches;
@@ -69,15 +69,33 @@ 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 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("branch") String branchName) throws IOException {
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
@@ -95,24 +113,42 @@ 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",
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 history(@PathParam("namespace") String namespace,
@PathParam("name") String name,
@PathParam("branch") String branchName,
@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();
@@ -143,15 +179,25 @@ 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)
@ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created branch"))
@Operation(summary = "Create branch", description = "Creates a new branch.", tags = "Repository")
@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")
@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 BranchRequestDto branchRequest) throws IOException {
@@ -195,15 +241,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();

View File

@@ -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;
@@ -44,15 +45,32 @@ public class ChangesetRootResource {
@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 = "Collection of changesets", description = "Returns a collection of changesets.", 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))) {
@@ -77,16 +95,33 @@ 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}")
@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();

View File

@@ -1,8 +1,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.Parameter;
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.config.ScmConfiguration;
import sonia.scm.repository.NamespaceStrategyValidator;
@@ -21,6 +25,9 @@ import javax.ws.rs.core.Response;
/**
* RESTful Web Service Resource to manage the configuration.
*/
@OpenAPIDefinition(tags = {
@Tag(name = "Instance configuration", description = "Global SCM-Manager instance configuration")
})
@Path(ConfigResource.CONFIG_PATH_V2)
public class ConfigResource {
@@ -46,13 +53,25 @@ public class ConfigResource {
@GET
@Path("")
@Produces(VndMediaType.CONFIG)
@TypeHint(UserDto.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:global\" privilege"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Operation(summary = "Instance configuration", description = "Returns the instance configuration.", tags = "Instance configuration")
@ApiResponse(
responseCode = "200",
description = "success",
content = @Content(
mediaType = VndMediaType.CONFIG,
schema = @Schema(implementation = ConfigDto.class)
)
)
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:read\" privilege")
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
public Response get() {
// We do this permission check in Resource and not in ScmConfiguration, because it must be available for reading
@@ -70,13 +89,18 @@ public class ConfigResource {
@PUT
@Path("")
@Consumes(VndMediaType.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:global\" privilege"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Operation(summary = "Update instance configuration", description = "Modifies the instance configuration.", tags = "Instance configuration")
@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\" privilege")
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
public Response update(@Valid ConfigDto configDto) {
// This *could* be moved to ScmConfiguration or ScmConfigurationUtil classes.

View File

@@ -2,8 +2,10 @@ package sonia.scm.api.v2.resources;
import com.github.sdorra.spotter.ContentType;
import com.github.sdorra.spotter.ContentTypes;
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.NotFoundException;
@@ -11,6 +13,7 @@ import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.util.IOUtil;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.ws.rs.GET;
@@ -43,20 +46,30 @@ public class ContentResource {
* recognized, this will be given in the header <code>Language</code>.
*
* @param namespace the namespace of the repository
* @param name the name of the repository
* @param revision the revision
* @param path The path of the file
*
* @param name the name of the repository
* @param revision the revision
* @param path The path of the file
*/
@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 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 = "File content by revision", description = "Returns the content of a file for the given revision in the repository.", tags = "Repository")
@ApiResponse(responseCode = "200", description = "success")
@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, @PathParam("revision") String revision, @PathParam("path") String path) {
StreamingOutput stream = createStreamingOutput(namespace, name, revision, path);
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
@@ -85,20 +98,34 @@ public class ContentResource {
* the repository. The programming language will be given in the header <code>Language</code>.
*
* @param namespace the namespace of the repository
* @param name the name of the repository
* @param revision the revision
* @param path The path of the file
*
* @param name the name of the repository
* @param revision the revision
* @param path The path of the file
*/
@HEAD
@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 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 = "File metadata by revision",
description = "Returns the content type and the programming language (if it can be detected) of a file for the given revision in the repository.",
tags = "Repository"
)
@ApiResponse(responseCode = "200", description = "success")
@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 metadata(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path) {
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
Response.ResponseBuilder responseBuilder = Response.ok();

View File

@@ -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 sonia.scm.NotFoundException;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.api.DiffCommandBuilder;
@@ -44,23 +46,35 @@ public class DiffRootResource {
* Get the repository diff of a revision
*
* @param namespace repository namespace
* @param name repository name
* @param revision the revision
* @param name repository name
* @param revision the revision
* @return the dif of the revision
* @throws NotFoundException if the repository is not found
*/
@GET
@Path("{revision}")
@Produces(VndMediaType.DIFF)
@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, no revision with the specified param for the repository available or repository not found"),
@ResponseCode(code = 500, condition = "internal server error")
})
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision , @Pattern(regexp = DIFF_FORMAT_VALUES_REGEX) @DefaultValue("NATIVE") @QueryParam("format") String format ) throws IOException {
@Operation(summary = "Diff by revision", description = "Get the repository diff of a revision.", tags = "Repository")
@ApiResponse(responseCode = "200", description = "success")
@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 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("revision") String revision, @Pattern(regexp = DIFF_FORMAT_VALUES_REGEX) @DefaultValue("NATIVE") @QueryParam("format") String format) throws IOException {
HttpUtil.checkForCRLFInjection(revision);
DiffFormat diffFormat = DiffFormat.valueOf(format);
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
@@ -77,14 +91,33 @@ public class DiffRootResource {
@GET
@Path("{revision}/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, no revision with the specified param for the repository available or repository not found"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Operation(summary = "Parsed diff by revision", description = "Get the parsed repository diff of a revision.", tags = "Repository")
@ApiResponse(
responseCode = "200",
description = "success",
content = @Content(
mediaType = VndMediaType.DIFF_PARSED,
schema = @Schema(implementation = DiffResultDto.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, 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",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
public DiffResultDto getParsed(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision) throws IOException {
HttpUtil.checkForCRLFInjection(revision);
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {

View File

@@ -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,

View File

@@ -1,5 +1,11 @@
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 sonia.scm.security.PermissionAssigner;
import sonia.scm.security.PermissionDescriptor;
import sonia.scm.web.VndMediaType;
@@ -10,6 +16,9 @@ import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
@OpenAPIDefinition(tags = {
@Tag(name = "Permissions", description = "Permission related endpoints")
})
@Path("v2/permissions")
public class GlobalPermissionResource {
@@ -22,6 +31,25 @@ public class GlobalPermissionResource {
@GET
@Produces(VndMediaType.PERMISSION_COLLECTION)
@Operation(summary = "List of permissions", description = "Returns all available permissions.", tags = "Permissions")
@ApiResponse(
responseCode = "200",
description = "success",
content = @Content(
mediaType = VndMediaType.PERMISSION_COLLECTION,
schema = @Schema(implementation = PermissionListDto.class)
)
)
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the permissions")
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
@Path("")
public Response getAll() {
String[] permissions = permissionAssigner.getAvailablePermissions().stream().map(PermissionDescriptor::getValue).toArray(String[]::new);

View File

@@ -1,10 +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.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.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 sonia.scm.group.Group;
import sonia.scm.group.GroupManager;
import sonia.scm.search.SearchRequest;
@@ -25,9 +25,8 @@ import java.util.function.Predicate;
import static com.google.common.base.Strings.isNullOrEmpty;
public class GroupCollectionResource {
private static final int DEFAULT_PAGE_SIZE = 10;
private final GroupDtoToGroupMapper dtoToGroupMapper;
private final GroupCollectionToDtoMapper groupCollectionToDtoMapper;
@@ -56,46 +55,70 @@ public class GroupCollectionResource {
@GET
@Path("")
@Produces(VndMediaType.GROUP_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 user does not have the \"group\" privilege"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Operation(summary = "List of groups", description = "Returns all groups for a given page number with a given page size.", tags = "Group")
@ApiResponse(
responseCode = "200",
description = "success",
content = @Content(
mediaType = VndMediaType.GROUP_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 user does not have the \"group\" 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,
@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 -> groupCollectionToDtoMapper.map(page, pageSize, pageResult));
pageResult -> groupCollectionToDtoMapper.map(page, pageSize, pageResult));
}
/**
* Creates a new group.
*
* @param group The group to be created.
* @return A response with the link to the new group (if created successfully).
*/
@POST
@Path("")
@Consumes(VndMediaType.GROUP)
@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 \"group\" privilege"),
@ResponseCode(code = 409, condition = "conflict, a group with this name already exists"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created group"))
@Operation(summary = "Create group", description = "Creates a new group.", tags = "Group")
@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")
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
public Response create(@Valid GroupDto group) {
return adapter.create(group,
() -> dtoToGroupMapper.map(group),
g -> resourceLinks.group().self(g.getName()));
() -> dtoToGroupMapper.map(group),
g -> resourceLinks.group().self(g.getName()));
}
private Predicate<Group> createSearchPredicate(String search) {

View File

@@ -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.security.PermissionAssigner;
import sonia.scm.security.PermissionDescriptor;
import sonia.scm.security.PermissionPermissions;
@@ -39,14 +40,31 @@ public class GroupPermissionResource {
@GET
@Path("")
@Produces(VndMediaType.PERMISSION_COLLECTION)
@TypeHint(PermissionListDto.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 group"),
@ResponseCode(code = 404, condition = "not found, no group with the specified id/name available"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Operation(summary = "Group permission", description = "Returns permissions for a group.", tags = {"Group", "Permissions"})
@ApiResponse(
responseCode = "200",
description = "success",
content = @Content(
mediaType = VndMediaType.PERMISSION_COLLECTION,
schema = @Schema(implementation = PermissionListDto.class)
))
@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",
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 getPermissions(@PathParam("id") String id) {
PermissionPermissions.read().check();
Collection<PermissionDescriptor> permissions = permissionAssigner.readPermissionsForGroup(id);
@@ -62,15 +80,26 @@ public class GroupPermissionResource {
@PUT
@Path("")
@Consumes(VndMediaType.PERMISSION_COLLECTION)
@StatusCodes({
@ResponseCode(code = 204, condition = "update success"),
@ResponseCode(code = 400, condition = "Invalid body"),
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@ResponseCode(code = 403, condition = "not authorized, the current group does not have the correct privilege"),
@ResponseCode(code = 404, condition = "not found, no group with the specified id/name available"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Operation(summary = "Update Group permissions", description = "Sets permissions for a group. Overwrites all existing permissions.", tags = {"Group", "Permissions"})
@ApiResponse(responseCode = "204", description = "update success")
@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",
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, PermissionListDto newPermissions) {
Collection<PermissionDescriptor> permissionDescriptors = Arrays.stream(newPermissions.getPermissions())
.map(PermissionDescriptor::new)

View File

@@ -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.group.Group;
import sonia.scm.group.GroupManager;
import sonia.scm.web.VndMediaType;
@@ -40,20 +41,37 @@ public class GroupResource {
* <strong>Note:</strong> This method requires "group" privilege.
*
* @param id the id/name of the group
*
*/
@GET
@Path("")
@Produces(VndMediaType.GROUP)
@TypeHint(GroupDto.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 group"),
@ResponseCode(code = 404, condition = "not found, no group with the specified id/name available"),
@ResponseCode(code = 500, condition = "internal server error")
})
public Response get(@PathParam("id") String id){
@Operation(summary = "Get single group", description = "Returns a group.", tags = "Group")
@ApiResponse(
responseCode = "200",
description = "success",
content = @Content(
mediaType = VndMediaType.GROUP,
schema = @Schema(implementation = GroupDto.class)
)
)
@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",
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("id") String id) {
return adapter.get(id, groupToGroupDtoMapper::map);
}
@@ -63,17 +81,21 @@ public class GroupResource {
* <strong>Note:</strong> This method requires "group" privilege.
*
* @param name the name of the group to delete.
*
*/
@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 \"group\" privilege"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@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")
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
public Response delete(@PathParam("id") String name) {
return adapter.delete(name);
}
@@ -83,21 +105,32 @@ public class GroupResource {
*
* <strong>Note:</strong> This method requires "group" privilege.
*
* @param name name of the group to be modified
* @param name name of the group to be modified
* @param group group object to modify
*/
@PUT
@Path("")
@Consumes(VndMediaType.GROUP)
@StatusCodes({
@ResponseCode(code = 204, condition = "update success"),
@ResponseCode(code = 400, condition = "Invalid body, e.g. illegal change of id/group name"),
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"group\" privilege"),
@ResponseCode(code = 404, condition = "not found, no group with the specified id/name available"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Operation(summary = "Update group", description = "Modifies a group.", tags = "Group")
@ApiResponse(responseCode = "204", description = "update success")
@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",
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 update(@PathParam("id") String name, @Valid GroupDto group) {
return adapter.update(name, existing -> dtoToGroupMapper.map(group));
}

View File

@@ -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 groups and their members.
*/
@OpenAPIDefinition(tags = {
@Tag(name = "Group", description = "Group related endpoints")
})
@Path(GroupRootResource.GROUPS_PATH_V2)
public class GroupRootResource {
@@ -17,7 +23,7 @@ public class GroupRootResource {
@Inject
public GroupRootResource(Provider<GroupCollectionResource> groupCollectionResource,
Provider<GroupResource> groupResource) {
Provider<GroupResource> groupResource) {
this.groupCollectionResource = groupCollectionResource;
this.groupResource = groupResource;
}

View File

@@ -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,

View File

@@ -1,6 +1,12 @@
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;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import sonia.scm.security.AllowAnonymousAccess;
import sonia.scm.web.VndMediaType;
@@ -9,6 +15,15 @@ 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")
},
tags = {
@Tag(name = "Index", description = "SCM-Manager Index")
}
)
@Path(IndexResource.INDEX_PATH_V2)
@AllowAnonymousAccess
public class IndexResource {
@@ -24,7 +39,23 @@ public class IndexResource {
@GET
@Path("")
@Produces(VndMediaType.INDEX)
@TypeHint(IndexDto.class)
@Operation(summary = "Get index", description = "Returns the index for the scm-manager instance.", tags = "Index")
@ApiResponse(
responseCode = "200",
description = "success",
content = @Content(
mediaType = VndMediaType.INDEX,
schema = @Schema(implementation = IndexDto.class)
)
)
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
public IndexDto getIndex() {
return indexDtoGenerator.generate();
}

View File

@@ -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 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.plugin.AvailablePlugin;
import sonia.scm.plugin.InstalledPlugin;
import sonia.scm.plugin.PluginManager;
@@ -43,12 +45,30 @@ public class InstalledPluginResource {
*/
@GET
@Path("")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(CollectionDto.class)
@Produces(VndMediaType.PLUGIN_COLLECTION)
@Operation(
summary = "Find all installed plugins",
description = "Returns a collection of installed plugins.",
tags = "Plugin Management"
)
@ApiResponse(
responseCode = "200",
description = "success",
content = @Content(
mediaType = VndMediaType.PLUGIN_COLLECTION,
schema = @Schema(implementation = HalRepresentation.class)
)
)
@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 = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
public Response getInstalledPlugins() {
PluginPermissions.read().check();
List<InstalledPlugin> plugins = pluginManager.getInstalled();
@@ -61,11 +81,22 @@ public class InstalledPluginResource {
*/
@POST
@Path("/update")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(CollectionDto.class)
@Operation(
summary = "Update all installed plugins",
description = "Updates all installed plugins to the latest compatible version.",
tags = "Plugin Management"
)
@ApiResponse(responseCode = "200", description = "success")
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:manage\" privilege")
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
public Response updateAll() {
pluginManager.updateAll();
return Response.ok().build();
@@ -75,18 +106,41 @@ public class InstalledPluginResource {
* Returns the installed plugin with the given id.
*
* @param name name of plugin
*
* @return installed plugin with specified id
*/
@GET
@Path("/{name}")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 404, condition = "not found"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(PluginDto.class)
@Produces(VndMediaType.PLUGIN)
@Operation(
summary = "Get installed plugin by name",
description = "Returns the installed plugin with the given id",
tags = "Plugin Management"
)
@ApiResponse(
responseCode = "200",
description = "success",
content = @Content(
mediaType = VndMediaType.PLUGIN,
schema = @Schema(implementation = PluginDto.class)
)
)
@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",
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("name") String name) {
PluginPermissions.read().check();
Optional<InstalledPlugin> pluginDto = pluginManager.getInstalled(name);
@@ -100,17 +154,29 @@ public class InstalledPluginResource {
/**
* Triggers plugin uninstall.
*
* @param name plugin name
* @return HTTP Status.
*/
@POST
@Path("/{name}/uninstall")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Operation(
summary = "Uninstall plugin",
description = "Add plugin uninstall to pending queue. The plugin will be removed on restart.",
tags = "Plugin Management"
)
@ApiResponse(responseCode = "200", description = "success")
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:manage\" privilege")
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
public Response uninstallPlugin(@PathParam("name") String name, @QueryParam("restart") boolean restartAfterInstallation) {
PluginPermissions.manage().check();
pluginManager.uninstall(name, restartAfterInstallation);
return Response.ok().build();
}

View File

@@ -1,8 +1,11 @@
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 +26,13 @@ import javax.ws.rs.core.UriInfo;
/**
* RESTful Web Service Resource to get currently logged in users.
*/
@OpenAPIDefinition(tags = {
@Tag(name = "Current user", description = "Current user 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 +50,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 = "Current user")
@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 +76,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 = "Current user")
@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()),

View File

@@ -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()

View File

@@ -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<String> availableStrategies = collectStrategyNames();

View File

@@ -1,10 +1,12 @@
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.Embedded;
import de.otto.edison.hal.HalRepresentation;
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.plugin.AvailablePlugin;
import sonia.scm.plugin.InstalledPlugin;
import sonia.scm.plugin.PluginManager;
@@ -40,11 +42,30 @@ public class PendingPluginResource {
@GET
@Path("")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces(VndMediaType.PLUGIN_COLLECTION)
@Operation(
summary = "Find all pending plugins",
description = "Returns a collection of pending plugins.",
tags = "Plugin Management"
)
@ApiResponse(
responseCode = "200",
description = "success",
content = @Content(
mediaType = VndMediaType.PLUGIN_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 \"plugin:read\" privilege")
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
public Response getPending() {
List<AvailablePlugin> pending = pluginManager
.getAvailable()
@@ -71,7 +92,7 @@ public class PendingPluginResource {
if (
PluginPermissions.manage().isPermitted() &&
(!installDtos.isEmpty() || !updateDtos.isEmpty() || !uninstallDtos.isEmpty())
(!installDtos.isEmpty() || !updateDtos.isEmpty() || !uninstallDtos.isEmpty())
) {
linksBuilder.single(link("execute", resourceLinks.pendingPluginCollection().executePending()));
linksBuilder.single(link("cancel", resourceLinks.pendingPluginCollection().cancelPending()));
@@ -103,10 +124,22 @@ public class PendingPluginResource {
@POST
@Path("/execute")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Operation(
summary = "Execute pending",
description = "Executes all pending plugin changes. The server will be restarted on this action.",
tags = "Plugin Management"
)
@ApiResponse(responseCode = "200", description = "success")
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:manage\" privilege")
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
public Response executePending() {
pluginManager.executePendingAndRestart();
return Response.ok().build();
@@ -114,10 +147,22 @@ public class PendingPluginResource {
@POST
@Path("/cancel")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Operation(
summary = "Cancel pending",
description = "Cancels all pending plugin changes and clear the pending queue.",
tags = "Plugin Management"
)
@ApiResponse(responseCode = "200", description = "success")
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:manage\" privilege")
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
public Response cancelPending() {
pluginManager.cancelPending();
return Response.ok().build();

View File

@@ -1,9 +1,15 @@
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;
@OpenAPIDefinition(tags = {
@Tag(name = "Plugin Management", description = "Plugin management related endpoints")
})
@Path("v2/plugins")
public class PluginRootResource {

View File

@@ -1,10 +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.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.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 org.apache.shiro.SecurityUtils;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryInitializer;
@@ -63,13 +63,24 @@ 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,15 +103,25 @@ 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)
@ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created repository"))
@Operation(summary = "Create repository", description = "Creates a new repository.", tags = "Repository")
@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")
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
))
public Response create(@Valid RepositoryDto repository, @QueryParam("initialize") boolean initialize) {
AtomicReference<Repository> reference = new AtomicReference<>();
Response response = adapter.create(repository,

View File

@@ -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) {

View File

@@ -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.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryManager;
@@ -87,14 +88,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 = "Get 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);
}
@@ -110,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));
}
@@ -133,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),
@@ -168,7 +196,7 @@ public class RepositoryResource {
}
@Path("branches/")
public BranchRootResource branches(@PathParam("namespace") String namespace, @PathParam("name") String name) {
public BranchRootResource branches() {
return branchRootResource.get();
}

View File

@@ -1,10 +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.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.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 sonia.scm.repository.RepositoryRole;
import sonia.scm.repository.RepositoryRoleManager;
import sonia.scm.web.VndMediaType;
@@ -51,21 +51,32 @@ 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,
@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));
}
/**
@@ -79,15 +90,25 @@ 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)
@ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created repositoryRole"))
@Operation(summary = "Create repository role", description = "Creates a new repository role.", tags = "Repository role")
@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")
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
))
public Response create(@Valid RepositoryRoleDto repositoryRole) {
return adapter.create(repositoryRole, () -> dtoToRepositoryRoleMapper.map(repositoryRole), u -> resourceLinks.repositoryRole().self(u.getName()));
}

View File

@@ -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));
}

View File

@@ -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 {

View File

@@ -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/";

View File

@@ -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());
}

View File

@@ -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,30 @@ 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",
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) {
for (RepositoryType type : repositoryManager.getConfiguredTypes()) {
if (name.equalsIgnoreCase(type.getName())) {

View File

@@ -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(),

View File

@@ -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);
}

View File

@@ -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)) {

View File

@@ -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<InstalledPlugin> 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> uiPluginDto = pluginLoader.getInstalledPlugins()
.stream()

View File

@@ -1,10 +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.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.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 org.apache.shiro.authc.credential.PasswordService;
import sonia.scm.search.SearchRequest;
import sonia.scm.search.SearchUtil;
@@ -59,22 +59,33 @@ public class UserCollectionResource {
@GET
@Path("")
@Produces(VndMediaType.USER_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 user does not have the \"user\" privilege"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Operation(summary = "List of users", description = "Returns all users for a given page number with a given page size.", tags = "User")
@ApiResponse(
responseCode = "200",
description = "success",
content = @Content(
mediaType = VndMediaType.USER_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 user does not have the \"user\" 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,
@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));
}
/**
@@ -88,15 +99,25 @@ public class UserCollectionResource {
@POST
@Path("")
@Consumes(VndMediaType.USER)
@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 \"user\" 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)
@ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created user"))
@Operation(summary = "Create user", description = "Creates a new user.", tags = "User")
@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")
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
))
public Response create(@Valid UserDto user) {
return adapter.create(user, () -> dtoToUserMapper.map(user, passwordService.encryptPassword(user.getPassword())), u -> resourceLinks.user().self(u.getName()));
}

View File

@@ -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.security.PermissionAssigner;
import sonia.scm.security.PermissionDescriptor;
import sonia.scm.security.PermissionPermissions;
@@ -40,14 +41,32 @@ public class UserPermissionResource {
@GET
@Path("")
@Produces(VndMediaType.PERMISSION_COLLECTION)
@TypeHint(PermissionListDto.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 user"),
@ResponseCode(code = 404, condition = "not found, no user with the specified id/name available"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Operation(summary = "User permission", description = "Returns the global git configuration.", tags = {"User", "Permissions"})
@ApiResponse(
responseCode = "200",
description = "success",
content = @Content(
mediaType = VndMediaType.PERMISSION_COLLECTION,
schema = @Schema(implementation = PermissionListDto.class)
)
)
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the user")
@ApiResponse(
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 getPermissions(@PathParam("id") String id) {
PermissionPermissions.read().check();
Collection<PermissionDescriptor> permissions = permissionAssigner.readPermissionsForUser(id);
@@ -63,15 +82,26 @@ public class UserPermissionResource {
@PUT
@Path("")
@Consumes(VndMediaType.PERMISSION_COLLECTION)
@StatusCodes({
@ResponseCode(code = 204, condition = "update success"),
@ResponseCode(code = 400, condition = "Invalid body"),
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the correct privilege"),
@ResponseCode(code = 404, condition = "not found, no user with the specified id/name available"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Operation(summary = "Update user permissions", description = "Sets permissions for a user. Overwrites all existing permissions.", tags = {"User", "Permissions"})
@ApiResponse(responseCode = "204", description = "update success")
@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",
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<PermissionDescriptor> permissionDescriptors = Arrays.stream(newPermissions.getPermissions())
.map(PermissionDescriptor::new)

View File

@@ -1,12 +1,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.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.authc.credential.PasswordService;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.user.UserPermissions;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
@@ -54,14 +54,31 @@ public class UserResource {
@GET
@Path("")
@Produces(VndMediaType.USER)
@TypeHint(UserDto.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 user"),
@ResponseCode(code = 404, condition = "not found, no user with the specified id/name available"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Operation(summary = "Get single user", description = "Returns the user for the given id.", tags = "User")
@ApiResponse(
responseCode = "200",
description = "success",
content = @Content(
mediaType = VndMediaType.USER,
schema = @Schema(implementation = UserDto.class)
)
)
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the user")
@ApiResponse(
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 get(@PathParam("id") String id) {
return adapter.get(id, userToDtoMapper::map);
}
@@ -75,13 +92,11 @@ public class UserResource {
*/
@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 \"user\" privilege"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@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")
@ApiResponse(responseCode = "500", description = "internal server error")
public Response delete(@PathParam("id") String name) {
return adapter.delete(name);
}
@@ -98,15 +113,19 @@ public class UserResource {
@PUT
@Path("")
@Consumes(VndMediaType.USER)
@StatusCodes({
@ResponseCode(code = 204, condition = "update success"),
@ResponseCode(code = 400, condition = "Invalid body, e.g. illegal change of id/user name"),
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"user\" privilege"),
@ResponseCode(code = 404, condition = "not found, no user with the specified id/name available"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@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")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"user\" privilege")
@ApiResponse(
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")
public Response update(@PathParam("id") String name, @Valid UserDto user) {
return adapter.update(name, existing -> dtoToUserMapper.map(user, existing.getPassword()));
}
@@ -125,15 +144,19 @@ public class UserResource {
@PUT
@Path("password")
@Consumes(VndMediaType.PASSWORD_OVERWRITE)
@StatusCodes({
@ResponseCode(code = 204, condition = "update success"),
@ResponseCode(code = 400, condition = "Invalid body, e.g. the user type is not xml or the given oldPassword do not match the stored one"),
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"user\" privilege"),
@ResponseCode(code = 404, condition = "not found, no user with the specified id/name available"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Operation(summary = "Modifies a user password", description = "Lets admins modifies the user password for the given id.", tags = "User")
@ApiResponse(responseCode = "204", description = "update success")
@ApiResponse(responseCode = "400", description = "invalid body, e.g. the user type is not xml or the given oldPassword do not match the stored one")
@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 = "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")
public Response overwritePassword(@PathParam("id") String name, @Valid PasswordOverwriteDto passwordOverwrite) {
userManager.overwritePassword(name, passwordService.encryptPassword(passwordOverwrite.getNewPassword()));
return Response.noContent().build();

View File

@@ -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 users.
*/
@OpenAPIDefinition(tags = {
@Tag(name = "User", description = "User related endpoints")
})
@Path(UserRootResource.USERS_PATH_V2)
public class UserRootResource {

View File

@@ -199,6 +199,10 @@
"8LRncum0S1": {
"displayName": "Interner Fehler im Repository",
"description": "Bei der Bearbeitung des internen Repositories ist ein Fehler oder ein unerwarteter Zustand aufgetreten. Bitte prüfen Sie die Logs für weitere Informationen."
},
"4GRrgkSC01": {
"displayName": "Unerwartetes Merge-Ergebnis",
"description": "Der Merge hatte ein unerwartetes Ergebis, das nicht automatisiert behandelt werden konnte. Nähere Details sind im Log zu finden. Führen Sie den Merge ggf. manuell durch."
}
},
"namespaceStrategies": {

View File

@@ -199,6 +199,10 @@
"8LRncum0S1": {
"displayName": "Internal repository error",
"description": "There was an error or an unexpected condition while handling the native repository. Please consult the logs for further information."
},
"4GRrgkSC01": {
"displayName": "Unexpected merge result",
"description": "The merge led to an unexpected result, that could not be handled automatically. More details could be found in the log. Please merge the branches manually."
}
},
"namespaceStrategies": {