merged 2.0.0-m3

This commit is contained in:
Maren Süwer
2018-12-19 09:58:47 +01:00
40 changed files with 641 additions and 112 deletions

View File

@@ -0,0 +1,9 @@
package sonia.scm;
import java.util.List;
public abstract class BadRequestException extends ExceptionWithContext {
public BadRequestException(List<ContextEntry> context, String message) {
super(context, message);
}
}

View File

@@ -40,13 +40,14 @@ import java.util.Collections;
* @author Sebastian Sdorra * @author Sebastian Sdorra
* @version 1.6 * @version 1.6
*/ */
public class NotSupportedFeatureException extends ExceptionWithContext { @SuppressWarnings("squid:MaximumInheritanceDepth") // exceptions have a deep inheritance depth themselves; therefore we accept this here
public class FeatureNotSupportedException extends BadRequestException {
private static final long serialVersionUID = 256498734456613496L; private static final long serialVersionUID = 256498734456613496L;
private static final String CODE = "9SR8G0kmU1"; private static final String CODE = "9SR8G0kmU1";
public NotSupportedFeatureException(String feature) public FeatureNotSupportedException(String feature)
{ {
super(Collections.emptyList(),createMessage(feature)); super(Collections.emptyList(),createMessage(feature));
} }

View File

@@ -38,7 +38,7 @@ package sonia.scm.repository;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.NotSupportedFeatureException; import sonia.scm.FeatureNotSupportedException;
import sonia.scm.SCMContextProvider; import sonia.scm.SCMContextProvider;
import sonia.scm.event.ScmEventBus; import sonia.scm.event.ScmEventBus;
@@ -167,12 +167,12 @@ public abstract class AbstractRepositoryHandler<C extends RepositoryConfig>
* *
* @return * @return
* *
* @throws NotSupportedFeatureException * @throws FeatureNotSupportedException
*/ */
@Override @Override
public ImportHandler getImportHandler() public ImportHandler getImportHandler()
{ {
throw new NotSupportedFeatureException("import"); throw new FeatureNotSupportedException("import");
} }
/** /**

View File

@@ -36,7 +36,7 @@ package sonia.scm.repository;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import sonia.scm.Handler; import sonia.scm.Handler;
import sonia.scm.NotSupportedFeatureException; import sonia.scm.FeatureNotSupportedException;
import sonia.scm.plugin.ExtensionPoint; import sonia.scm.plugin.ExtensionPoint;
/** /**
@@ -59,9 +59,9 @@ public interface RepositoryHandler
* @return {@link ImportHandler} for the repository type of this handler * @return {@link ImportHandler} for the repository type of this handler
* @since 1.12 * @since 1.12
* *
* @throws NotSupportedFeatureException * @throws FeatureNotSupportedException
*/ */
public ImportHandler getImportHandler() throws NotSupportedFeatureException; public ImportHandler getImportHandler() throws FeatureNotSupportedException;
/** /**
* Returns informations about the version of the RepositoryHandler. * Returns informations about the version of the RepositoryHandler.

View File

@@ -38,7 +38,7 @@ package sonia.scm.repository.api;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.NotSupportedFeatureException; import sonia.scm.FeatureNotSupportedException;
import sonia.scm.repository.Feature; import sonia.scm.repository.Feature;
import sonia.scm.repository.spi.DiffCommand; import sonia.scm.repository.spi.DiffCommand;
import sonia.scm.repository.spi.DiffCommandRequest; import sonia.scm.repository.spi.DiffCommandRequest;
@@ -203,7 +203,7 @@ public final class DiffCommandBuilder
public DiffCommandBuilder setAncestorChangeset(String revision) public DiffCommandBuilder setAncestorChangeset(String revision)
{ {
if (!supportedFeatures.contains(Feature.INCOMING_REVISION)) { if (!supportedFeatures.contains(Feature.INCOMING_REVISION)) {
throw new NotSupportedFeatureException(Feature.INCOMING_REVISION.name()); throw new FeatureNotSupportedException(Feature.INCOMING_REVISION.name());
} }
request.setAncestorChangeset(revision); request.setAncestorChangeset(revision);

View File

@@ -39,7 +39,7 @@ import com.google.common.base.Objects;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.NotSupportedFeatureException; import sonia.scm.FeatureNotSupportedException;
import sonia.scm.cache.Cache; import sonia.scm.cache.Cache;
import sonia.scm.cache.CacheManager; import sonia.scm.cache.CacheManager;
import sonia.scm.repository.Changeset; import sonia.scm.repository.Changeset;
@@ -410,7 +410,7 @@ public final class LogCommandBuilder
*/ */
public LogCommandBuilder setAncestorChangeset(String ancestorChangeset) { public LogCommandBuilder setAncestorChangeset(String ancestorChangeset) {
if (!supportedFeatures.contains(Feature.INCOMING_REVISION)) { if (!supportedFeatures.contains(Feature.INCOMING_REVISION)) {
throw new NotSupportedFeatureException(Feature.INCOMING_REVISION.name()); throw new FeatureNotSupportedException(Feature.INCOMING_REVISION.name());
} }
request.setAncestorChangeset(ancestorChangeset); request.setAncestorChangeset(ancestorChangeset);
return this; return this;

View File

@@ -80,8 +80,20 @@ public interface AccessToken {
*/ */
Date getExpiration(); Date getExpiration();
/**
* Returns refresh expiration of token.
*
* @return refresh expiration
*/
Optional<Date> getRefreshExpiration(); Optional<Date> getRefreshExpiration();
/**
* Returns id of the parent key.
*
* @return parent key id
*/
Optional<String> getParentKey();
/** /**
* Returns the scope of the token. The scope is able to reduce the permissions of the subject in the context of this * Returns the scope of the token. The scope is able to reduce the permissions of the subject in the context of this
* token. For example we could issue a token which can only be used to read a single repository. for more informations * token. For example we could issue a token which can only be used to read a single repository. for more informations

View File

@@ -0,0 +1,30 @@
package sonia.scm.security;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Generates cookies and invalidates access token cookies.
*
* @author Sebastian Sdorra
* @since 2.0.0
*/
public interface AccessTokenCookieIssuer {
/**
* Creates a cookie for token authentication and attaches it to the response.
*
* @param request http servlet request
* @param response http servlet response
* @param accessToken access token
*/
void authenticate(HttpServletRequest request, HttpServletResponse response, AccessToken accessToken);
/**
* Invalidates the authentication cookie.
*
* @param request http servlet request
* @param response http servlet response
*/
void invalidate(HttpServletRequest request, HttpServletResponse response);
}

View File

@@ -164,7 +164,7 @@ public class DefaultCipherHandler implements CipherHandler {
String result = null; String result = null;
try { try {
byte[] encodedInput = Base64.getDecoder().decode(value); byte[] encodedInput = Base64.getUrlDecoder().decode(value);
byte[] salt = new byte[SALT_LENGTH]; byte[] salt = new byte[SALT_LENGTH];
byte[] encoded = new byte[encodedInput.length - SALT_LENGTH]; byte[] encoded = new byte[encodedInput.length - SALT_LENGTH];
@@ -221,7 +221,7 @@ public class DefaultCipherHandler implements CipherHandler {
System.arraycopy(salt, 0, result, 0, SALT_LENGTH); System.arraycopy(salt, 0, result, 0, SALT_LENGTH);
System.arraycopy(encodedInput, 0, result, SALT_LENGTH, System.arraycopy(encodedInput, 0, result, SALT_LENGTH,
result.length - SALT_LENGTH); result.length - SALT_LENGTH);
res = new String(Base64.getEncoder().encode(result), ENCODING); res = new String(Base64.getUrlEncoder().encode(result), ENCODING);
} catch (IOException | GeneralSecurityException ex) { } catch (IOException | GeneralSecurityException ex) {
throw new CipherException("could not encode string", ex); throw new CipherException("could not encode string", ex);
} }

View File

@@ -33,6 +33,10 @@
package sonia.scm.store; package sonia.scm.store;
import java.util.Optional;
import static java.util.Optional.ofNullable;
/** /**
* ConfigurationStore for configuration objects. <strong>Note:</strong> the default * ConfigurationStore for configuration objects. <strong>Note:</strong> the default
* implementation use JAXB to marshall the configuration objects. * implementation use JAXB to marshall the configuration objects.
@@ -50,7 +54,17 @@ public interface ConfigurationStore<T>
* *
* @return configuration object from store * @return configuration object from store
*/ */
public T get(); T get();
/**
* Returns the configuration object from store.
*
*
* @return configuration object from store
*/
default Optional<T> getOptional() {
return ofNullable(get());
}
//~--- set methods ---------------------------------------------------------- //~--- set methods ----------------------------------------------------------
@@ -60,5 +74,5 @@ public interface ConfigurationStore<T>
* *
* @param obejct configuration object to store * @param obejct configuration object to store
*/ */
public void set(T obejct); void set(T object);
} }

View File

@@ -32,6 +32,10 @@
package sonia.scm.store; package sonia.scm.store;
import java.util.Optional;
import static java.util.Optional.ofNullable;
/** /**
* Base class for {@link BlobStore} and {@link DataStore}. * Base class for {@link BlobStore} and {@link DataStore}.
* *
@@ -67,4 +71,16 @@ public interface MultiEntryStore<T> {
* @return item with the given id * @return item with the given id
*/ */
public T get(String id); public T get(String id);
/**
* Returns the item with the given id from the store.
*
*
* @param id id of the item to return
*
* @return item with the given id
*/
default Optional<T> getOptional(String id) {
return ofNullable(get(id));
}
} }

View File

@@ -1,12 +1,13 @@
package sonia.scm.user; package sonia.scm.user;
import sonia.scm.BadRequestException;
import sonia.scm.ContextEntry; import sonia.scm.ContextEntry;
import sonia.scm.ExceptionWithContext;
public class ChangePasswordNotAllowedException extends ExceptionWithContext { @SuppressWarnings("squid:MaximumInheritanceDepth") // exceptions have a deep inheritance depth themselves; therefore we accept this here
public class ChangePasswordNotAllowedException extends BadRequestException {
private static final String CODE = "9BR7qpDAe1"; private static final String CODE = "9BR7qpDAe1";
public static final String WRONG_USER_TYPE = "User of type %s are not allowed to change password"; public static final String WRONG_USER_TYPE = "Users of type %s are not allowed to change password";
public ChangePasswordNotAllowedException(ContextEntry.ContextBuilder context, String type) { public ChangePasswordNotAllowedException(ContextEntry.ContextBuilder context, String type) {
super(context.build(), String.format(WRONG_USER_TYPE, type)); super(context.build(), String.format(WRONG_USER_TYPE, type));

View File

@@ -1,9 +1,10 @@
package sonia.scm.user; package sonia.scm.user;
import sonia.scm.BadRequestException;
import sonia.scm.ContextEntry; import sonia.scm.ContextEntry;
import sonia.scm.ExceptionWithContext;
public class InvalidPasswordException extends ExceptionWithContext { @SuppressWarnings("squid:MaximumInheritanceDepth") // exceptions have a deep inheritance depth themselves; therefore we accept this here
public class InvalidPasswordException extends BadRequestException {
private static final String CODE = "8YR7aawFW1"; private static final String CODE = "8YR7aawFW1";

View File

@@ -0,0 +1,40 @@
package sonia.scm.web;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import static java.util.Collections.singletonMap;
import static sonia.scm.web.VndMediaType.REPOSITORY;
import static sonia.scm.web.VndMediaType.REPOSITORY_COLLECTION;
public abstract class AbstractRepositoryJsonEnricher extends JsonEnricherBase {
public AbstractRepositoryJsonEnricher(ObjectMapper objectMapper) {
super(objectMapper);
}
@Override
public void enrich(JsonEnricherContext context) {
if (resultHasMediaType(REPOSITORY, context)) {
JsonNode repositoryNode = context.getResponseEntity();
enrichRepositoryNode(repositoryNode);
} else if (resultHasMediaType(REPOSITORY_COLLECTION, context)) {
JsonNode repositoryCollectionNode = context.getResponseEntity().get("_embedded").withArray("repositories");
repositoryCollectionNode.elements().forEachRemaining(this::enrichRepositoryNode);
}
}
private void enrichRepositoryNode(JsonNode repositoryNode) {
String namespace = repositoryNode.get("namespace").asText();
String name = repositoryNode.get("name").asText();
enrichRepositoryNode(repositoryNode, namespace, name);
}
protected abstract void enrichRepositoryNode(JsonNode repositoryNode, String namespace, String name);
protected void addLink(JsonNode repositoryNode, String linkName, String link) {
JsonNode hrefNode = createObject(singletonMap("href", value(link)));
addPropertyNode(repositoryNode.get("_links"), linkName, hrefNode);
}
}

View File

@@ -0,0 +1,25 @@
package sonia.scm.xml;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
/**
* JAXB adapter for {@link Instant} objects.
*
* @since 2.0.0
*/
public class XmlInstantAdapter extends XmlAdapter<String, Instant> {
@Override
public String marshal(Instant instant) {
return DateTimeFormatter.ISO_INSTANT.format(instant);
}
@Override
public Instant unmarshal(String text) {
TemporalAccessor parsed = DateTimeFormatter.ISO_INSTANT.parse(text);
return Instant.from(parsed);
}
}

View File

@@ -0,0 +1,107 @@
package sonia.scm.web;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.io.Resources;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.api.v2.resources.ScmPathInfoStore;
import javax.ws.rs.core.MediaType;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(MockitoExtension.class)
class AbstractRepositoryJsonEnricherTest {
private final ObjectMapper objectMapper = new ObjectMapper();
private AbstractRepositoryJsonEnricher linkEnricher;
private JsonNode rootNode;
@BeforeEach
void globalSetUp() {
ScmPathInfoStore pathInfoStore = new ScmPathInfoStore();
pathInfoStore.set(() -> URI.create("/"));
linkEnricher = new AbstractRepositoryJsonEnricher(objectMapper) {
@Override
protected void enrichRepositoryNode(JsonNode repositoryNode, String namespace, String name) {
addLink(repositoryNode, "new-link", "/somewhere");
}
};
}
@Test
void shouldEnrichRepositories() throws IOException {
URL resource = Resources.getResource("sonia/scm/repository/repository-001.json");
rootNode = objectMapper.readTree(resource);
JsonEnricherContext context = new JsonEnricherContext(
URI.create("/"),
MediaType.valueOf(VndMediaType.REPOSITORY),
rootNode
);
linkEnricher.enrich(context);
String configLink = context.getResponseEntity()
.get("_links")
.get("new-link")
.get("href")
.asText();
assertThat(configLink).isEqualTo("/somewhere");
}
@Test
void shouldEnrichAllRepositories() throws IOException {
URL resource = Resources.getResource("sonia/scm/repository/repository-collection-001.json");
rootNode = objectMapper.readTree(resource);
JsonEnricherContext context = new JsonEnricherContext(
URI.create("/"),
MediaType.valueOf(VndMediaType.REPOSITORY_COLLECTION),
rootNode
);
linkEnricher.enrich(context);
context.getResponseEntity()
.get("_embedded")
.withArray("repositories")
.elements()
.forEachRemaining(node -> {
String configLink = node
.get("_links")
.get("new-link")
.get("href")
.asText();
assertThat(configLink).isEqualTo("/somewhere");
});
}
@Test
void shouldNotModifyObjectsWithUnsupportedMediaType() throws IOException {
URL resource = Resources.getResource("sonia/scm/repository/repository-001.json");
rootNode = objectMapper.readTree(resource);
JsonEnricherContext context = new JsonEnricherContext(
URI.create("/"),
MediaType.valueOf(VndMediaType.USER),
rootNode
);
linkEnricher.enrich(context);
boolean hasNewPullRequestLink = context.getResponseEntity()
.get("_links")
.has("new-link");
assertThat(hasNewPullRequestLink).isFalse();
}
}

View File

@@ -0,0 +1,47 @@
package sonia.scm.xml;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import javax.xml.bind.JAXB;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import java.nio.file.Path;
import java.time.Instant;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(TempDirectory.class)
class XmlInstantAdapterTest {
@Test
void shouldMarshalAndUnmarshalInstant(@TempDirectory.TempDir Path tempDirectory) {
Path path = tempDirectory.resolve("instant.xml");
Instant instant = Instant.now();
InstantObject object = new InstantObject(instant);
JAXB.marshal(object, path.toFile());
InstantObject unmarshaled = JAXB.unmarshal(path.toFile(), InstantObject.class);
assertEquals(instant, unmarshaled.instant);
}
@XmlRootElement(name = "instant-object")
@XmlAccessorType(XmlAccessType.FIELD)
public static class InstantObject {
@XmlJavaTypeAdapter(XmlInstantAdapter.class)
private Instant instant;
public InstantObject() {
}
InstantObject(Instant instant) {
this.instant = instant;
}
}
}

View File

@@ -0,0 +1,42 @@
{
"creationDate": "2018-11-09T09:48:32.732Z",
"description": "Handling static webresources made easy",
"healthCheckFailures": [],
"lastModified": "2018-11-09T09:49:20.973Z",
"namespace": "scmadmin",
"name": "web-resources",
"archived": false,
"type": "git",
"_links": {
"self": {
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
},
"delete": {
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
},
"update": {
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
},
"permissions": {
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/permissions/"
},
"protocol": [
{
"href": "http://localhost:8081/scm/repo/scmadmin/web-resources",
"name": "http"
}
],
"tags": {
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/tags/"
},
"branches": {
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/branches/"
},
"changesets": {
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/changesets/"
},
"sources": {
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/sources/"
}
}
}

View File

@@ -0,0 +1,106 @@
{
"page": 0,
"pageTotal": 1,
"_links": {
"self": {
"href": "http://localhost:8081/scm/api/v2/repositories/?page=0&pageSize=10"
},
"first": {
"href": "http://localhost:8081/scm/api/v2/repositories/?page=0&pageSize=10"
},
"last": {
"href": "http://localhost:8081/scm/api/v2/repositories/?page=0&pageSize=10"
},
"create": {
"href": "http://localhost:8081/scm/api/v2/repositories/"
}
},
"_embedded": {
"repositories": [
{
"creationDate": "2018-11-09T09:48:32.732Z",
"description": "Handling static webresources made easy",
"healthCheckFailures": [],
"lastModified": "2018-11-09T09:49:20.973Z",
"namespace": "scmadmin",
"name": "web-resources",
"archived": false,
"type": "git",
"_links": {
"self": {
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
},
"delete": {
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
},
"update": {
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
},
"permissions": {
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/permissions/"
},
"protocol": [
{
"href": "http://localhost:8081/scm/repo/scmadmin/web-resources",
"name": "http"
}
],
"tags": {
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/tags/"
},
"branches": {
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/branches/"
},
"changesets": {
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/changesets/"
},
"sources": {
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/sources/"
}
}
},
{
"creationDate": "2018-11-09T09:48:32.732Z",
"description": "Handling static webresources made easy",
"healthCheckFailures": [],
"lastModified": "2018-11-09T09:49:20.973Z",
"namespace": "scmadmin",
"name": "web-resources",
"archived": false,
"type": "git",
"_links": {
"self": {
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
},
"delete": {
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
},
"update": {
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
},
"permissions": {
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/permissions/"
},
"protocol": [
{
"href": "http://localhost:8081/scm/repo/scmadmin/web-resources",
"name": "http"
}
],
"tags": {
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/tags/"
},
"branches": {
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/branches/"
},
"changesets": {
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/changesets/"
},
"sources": {
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/sources/"
}
}
}
]
}
}

View File

@@ -58,8 +58,6 @@ public abstract class FileBasedStoreFactory {
private RepositoryLocationResolver repositoryLocationResolver; private RepositoryLocationResolver repositoryLocationResolver;
private Store store; private Store store;
private File storeDirectory;
protected FileBasedStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, Store store) { protected FileBasedStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, Store store) {
this.contextProvider = contextProvider; this.contextProvider = contextProvider;
this.repositoryLocationResolver = repositoryLocationResolver; this.repositoryLocationResolver = repositoryLocationResolver;
@@ -75,7 +73,7 @@ public abstract class FileBasedStoreFactory {
} }
protected File getStoreLocation(String name, Class type, Repository repository) { protected File getStoreLocation(String name, Class type, Repository repository) {
if (storeDirectory == null) { File storeDirectory;
if (repository != null) { if (repository != null) {
LOG.debug("create store with type: {}, name: {} and repository: {}", type, name, repository.getNamespaceAndName()); LOG.debug("create store with type: {}, name: {} and repository: {}", type, name, repository.getNamespaceAndName());
storeDirectory = this.getStoreDirectory(store, repository); storeDirectory = this.getStoreDirectory(store, repository);
@@ -84,8 +82,7 @@ public abstract class FileBasedStoreFactory {
storeDirectory = this.getStoreDirectory(store); storeDirectory = this.getStoreDirectory(store);
} }
IOUtil.mkdirs(storeDirectory); IOUtil.mkdirs(storeDirectory);
} return new File(storeDirectory, name);
return new File(this.storeDirectory, name);
} }
/** /**

View File

@@ -42,8 +42,20 @@ package sonia.scm.store;
*/ */
public class InMemoryConfigurationStoreFactory implements ConfigurationStoreFactory { public class InMemoryConfigurationStoreFactory implements ConfigurationStoreFactory {
private ConfigurationStore store;
public InMemoryConfigurationStoreFactory() {
}
public InMemoryConfigurationStoreFactory(ConfigurationStore store) {
this.store = store;
}
@Override @Override
public ConfigurationStore getStore(TypedStoreParameters storeParameters) { public ConfigurationStore getStore(TypedStoreParameters storeParameters) {
if (store != null) {
return store;
}
return new InMemoryConfigurationStore<>(); return new InMemoryConfigurationStore<>();
} }
} }

View File

@@ -0,0 +1,53 @@
package sonia.scm.store;
import sonia.scm.security.KeyGenerator;
import sonia.scm.security.UUIDKeyGenerator;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* In memory store implementation of {@link DataStore}.
*
* @author Sebastian Sdorra
*
* @param <T> type of stored object
*/
public class InMemoryDataStore<T> implements DataStore<T> {
private final Map<String, T> store = new HashMap<>();
private KeyGenerator generator = new UUIDKeyGenerator();
@Override
public String put(T item) {
String key = generator.createKey();
store.put(key, item);
return key;
}
@Override
public void put(String id, T item) {
store.put(id, item);
}
@Override
public Map<String, T> getAll() {
return Collections.unmodifiableMap(store);
}
@Override
public void clear() {
store.clear();
}
@Override
public void remove(String id) {
store.remove(id);
}
@Override
public T get(String id) {
return store.get(id);
}
}

View File

@@ -0,0 +1,26 @@
package sonia.scm.store;
/**
* In memory configuration store factory for testing purposes.
*
* @author Sebastian Sdorra
*/
public class InMemoryDataStoreFactory implements DataStoreFactory {
private InMemoryDataStore store;
public InMemoryDataStoreFactory() {
}
public InMemoryDataStoreFactory(InMemoryDataStore store) {
this.store = store;
}
@Override
public <T> DataStore<T> getStore(TypedStoreParameters<T> storeParameters) {
if (store != null) {
return store;
}
return new InMemoryDataStore<>();
}
}

View File

@@ -64,7 +64,8 @@ class ConfigurationBinder {
// route for global configuration, passes the current repository to component // route for global configuration, passes the current repository to component
const RepoRoute = ({url, repository}) => { const RepoRoute = ({url, repository}) => {
return this.route(url + to, <RepositoryComponent repository={repository}/>); const link = repository._links[linkName].href
return this.route(url + to, <RepositoryComponent repository={repository} link={link}/>);
}; };
// bind config route to extension point // bind config route to extension point

View File

@@ -114,7 +114,7 @@ class RepositoryRoot extends React.Component<Props> {
return ( return (
<Page title={repository.namespace + "/" + repository.name}> <Page title={repository.namespace + "/" + repository.name}>
<div className="columns"> <div className="columns">
<div className="column is-three-quarters"> <div className="column is-three-quarters is-clipped">
<Switch> <Switch>
<Route <Route
path={url} path={url}

View File

@@ -119,7 +119,9 @@ class FileTree extends React.Component<Props> {
<th className="is-hidden-mobile"> <th className="is-hidden-mobile">
{t("sources.file-tree.lastModified")} {t("sources.file-tree.lastModified")}
</th> </th>
<th>{t("sources.file-tree.description")}</th> <th className="is-hidden-mobile">
{t("sources.file-tree.description")}
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View File

@@ -6,10 +6,14 @@ import FileSize from "./FileSize";
import FileIcon from "./FileIcon"; import FileIcon from "./FileIcon";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import type { File } from "@scm-manager/ui-types"; import type { File } from "@scm-manager/ui-types";
import classNames from "classnames";
const styles = { const styles = {
iconColumn: { iconColumn: {
width: "16px" width: "16px"
},
wordBreakMinWidth: {
minWidth: "10em"
} }
}; };
@@ -71,12 +75,14 @@ class FileTreeLeaf extends React.Component<Props> {
return ( return (
<tr> <tr>
<td className={classes.iconColumn}>{this.createFileIcon(file)}</td> <td className={classes.iconColumn}>{this.createFileIcon(file)}</td>
<td>{this.createFileName(file)}</td> <td className={classNames(classes.wordBreakMinWidth, "is-word-break")}>{this.createFileName(file)}</td>
<td className="is-hidden-mobile">{fileSize}</td> <td className="is-hidden-mobile">{fileSize}</td>
<td className="is-hidden-mobile"> <td className="is-hidden-mobile">
<DateFromNow date={file.lastModified} /> <DateFromNow date={file.lastModified} />
</td> </td>
<td>{file.description}</td> <td className={classNames(classes.wordBreakMinWidth, "is-word-break", "is-hidden-mobile")}>
{file.description}
</td>
</tr> </tr>
); );
} }

View File

@@ -41,6 +41,13 @@ const styles = {
isVerticalCenter: { isVerticalCenter: {
display: "flex", display: "flex",
alignItems: "center" alignItems: "center"
},
wordBreak: {
WebkitHyphens: "auto",
MozHyphens: "auto",
MsHyphens: "auto",
hypens: "auto",
wordBreak: "break-all",
} }
}; };
@@ -93,7 +100,7 @@ class Content extends React.Component<Props, State> {
classes.marginInHeader classes.marginInHeader
)} )}
/> />
<span>{file.name}</span> <span className={classes.wordBreak}>{file.name}</span>
</div> </div>
<div className="media-right">{selector}</div> <div className="media-right">{selector}</div>
</article> </article>
@@ -125,11 +132,11 @@ class Content extends React.Component<Props, State> {
<tbody> <tbody>
<tr> <tr>
<td>{t("sources.content.path")}</td> <td>{t("sources.content.path")}</td>
<td>{file.path}</td> <td className={classes.wordBreak}>{file.path}</td>
</tr> </tr>
<tr> <tr>
<td>{t("sources.content.branch")}</td> <td>{t("sources.content.branch")}</td>
<td>{revision}</td> <td className={classes.wordBreak}>{revision}</td>
</tr> </tr>
<tr> <tr>
<td>{t("sources.content.size")}</td> <td>{t("sources.content.size")}</td>
@@ -141,7 +148,7 @@ class Content extends React.Component<Props, State> {
</tr> </tr>
<tr> <tr>
<td>{t("sources.content.description")}</td> <td>{t("sources.content.description")}</td>
<td>{description}</td> <td className={classes.wordBreak}>{description}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -27,6 +27,14 @@ $blue: #33B2E8;
padding: 0 0 0 3.8em !important; padding: 0 0 0 3.8em !important;
} }
.is-word-break {
-webkit-hyphens: auto;
-moz-hyphens: auto;
-ms-hyphens: auto;
hyphens: auto;
word-break: break-all;
}
.main { .main {
min-height: calc(100vh - 260px); min-height: calc(100vh - 260px);
} }

View File

@@ -79,14 +79,14 @@ import sonia.scm.repository.spi.HookEventFacade;
import sonia.scm.repository.xml.XmlRepositoryDAO; import sonia.scm.repository.xml.XmlRepositoryDAO;
import sonia.scm.schedule.QuartzScheduler; import sonia.scm.schedule.QuartzScheduler;
import sonia.scm.schedule.Scheduler; import sonia.scm.schedule.Scheduler;
import sonia.scm.security.AccessTokenCookieIssuer;
import sonia.scm.security.AuthorizationChangedEventProducer; import sonia.scm.security.AuthorizationChangedEventProducer;
import sonia.scm.security.CipherHandler; import sonia.scm.security.CipherHandler;
import sonia.scm.security.CipherUtil; import sonia.scm.security.CipherUtil;
import sonia.scm.security.ConfigurableLoginAttemptHandler; import sonia.scm.security.ConfigurableLoginAttemptHandler;
import sonia.scm.security.DefaultJwtAccessTokenRefreshStrategy; import sonia.scm.security.DefaultAccessTokenCookieIssuer;
import sonia.scm.security.DefaultKeyGenerator; import sonia.scm.security.DefaultKeyGenerator;
import sonia.scm.security.DefaultSecuritySystem; import sonia.scm.security.DefaultSecuritySystem;
import sonia.scm.security.JwtAccessTokenRefreshStrategy;
import sonia.scm.security.KeyGenerator; import sonia.scm.security.KeyGenerator;
import sonia.scm.security.LoginAttemptHandler; import sonia.scm.security.LoginAttemptHandler;
import sonia.scm.security.SecuritySystem; import sonia.scm.security.SecuritySystem;
@@ -320,6 +320,7 @@ public class ScmServletModule extends ServletModule
// bind events // bind events
// bind(LastModifiedUpdateListener.class); // bind(LastModifiedUpdateListener.class);
bind(AccessTokenCookieIssuer.class).to(DefaultAccessTokenCookieIssuer.class);
bind(PushStateDispatcher.class).toProvider(PushStateDispatcherProvider.class); bind(PushStateDispatcher.class).toProvider(PushStateDispatcherProvider.class);
} }

View File

@@ -0,0 +1,16 @@
package sonia.scm.api.rest;
import sonia.scm.BadRequestException;
import sonia.scm.api.v2.resources.ExceptionWithContextToErrorDtoMapper;
import javax.inject.Inject;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
@Provider
public class BadRequestExceptionMapper extends ContextualExceptionMapper<BadRequestException> {
@Inject
public BadRequestExceptionMapper(ExceptionWithContextToErrorDtoMapper mapper) {
super(BadRequestException.class, Response.Status.BAD_REQUEST, mapper);
}
}

View File

@@ -46,7 +46,7 @@ import org.apache.shiro.SecurityUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.NotFoundException; import sonia.scm.NotFoundException;
import sonia.scm.NotSupportedFeatureException; import sonia.scm.FeatureNotSupportedException;
import sonia.scm.Type; import sonia.scm.Type;
import sonia.scm.api.rest.RestActionUploadResult; import sonia.scm.api.rest.RestActionUploadResult;
import sonia.scm.api.v2.resources.RepositoryResource; import sonia.scm.api.v2.resources.RepositoryResource;
@@ -394,7 +394,7 @@ public class RepositoryImportResource
response = Response.ok(result).build(); response = Response.ok(result).build();
} }
catch (NotSupportedFeatureException ex) catch (FeatureNotSupportedException ex)
{ {
logger logger
.warn( .warn(
@@ -609,7 +609,7 @@ public class RepositoryImportResource
types.add(t); types.add(t);
} }
} }
catch (NotSupportedFeatureException ex) catch (FeatureNotSupportedException ex)
{ {
if (logger.isTraceEnabled()) if (logger.isTraceEnabled())
{ {
@@ -711,7 +711,7 @@ public class RepositoryImportResource
} }
} }
} }
catch (NotSupportedFeatureException ex) catch (FeatureNotSupportedException ex)
{ {
throw new WebApplicationException(ex, Response.Status.BAD_REQUEST); throw new WebApplicationException(ex, Response.Status.BAD_REQUEST);
} }

View File

@@ -1,17 +0,0 @@
package sonia.scm.api.v2;
import sonia.scm.NotSupportedFeatureException;
import sonia.scm.api.rest.ContextualExceptionMapper;
import sonia.scm.api.v2.resources.ExceptionWithContextToErrorDtoMapper;
import javax.inject.Inject;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
@Provider
public class NotSupportedFeatureExceptionMapper extends ContextualExceptionMapper<NotSupportedFeatureException> {
@Inject
public NotSupportedFeatureExceptionMapper(ExceptionWithContextToErrorDtoMapper mapper) {
super(NotSupportedFeatureException.class, Response.Status.BAD_REQUEST, mapper);
}
}

View File

@@ -1,17 +0,0 @@
package sonia.scm.api.v2.resources;
import sonia.scm.api.rest.ContextualExceptionMapper;
import sonia.scm.user.ChangePasswordNotAllowedException;
import sonia.scm.user.InvalidPasswordException;
import javax.inject.Inject;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
@Provider
public class ChangePasswordNotAllowedExceptionMapper extends ContextualExceptionMapper<ChangePasswordNotAllowedException> {
@Inject
public ChangePasswordNotAllowedExceptionMapper(ExceptionWithContextToErrorDtoMapper mapper) {
super(ChangePasswordNotAllowedException.class, Response.Status.BAD_REQUEST, mapper);
}
}

View File

@@ -1,17 +0,0 @@
package sonia.scm.api.v2.resources;
import sonia.scm.api.rest.ContextualExceptionMapper;
import sonia.scm.user.InvalidPasswordException;
import javax.inject.Inject;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
@Provider
public class InvalidPasswordExceptionMapper extends ContextualExceptionMapper<InvalidPasswordException> {
@Inject
public InvalidPasswordExceptionMapper(ExceptionWithContextToErrorDtoMapper mapper) {
super(InvalidPasswordException.class, Response.Status.BAD_REQUEST, mapper);
}
}

View File

@@ -51,12 +51,12 @@ import java.util.concurrent.TimeUnit;
* @author Sebastian Sdorra * @author Sebastian Sdorra
* @since 2.0.0 * @since 2.0.0
*/ */
public final class AccessTokenCookieIssuer { public final class DefaultAccessTokenCookieIssuer implements AccessTokenCookieIssuer {
/** /**
* the logger for AccessTokenCookieIssuer * the logger for DefaultAccessTokenCookieIssuer
*/ */
private static final Logger LOG = LoggerFactory.getLogger(AccessTokenCookieIssuer.class); private static final Logger LOG = LoggerFactory.getLogger(DefaultAccessTokenCookieIssuer.class);
private final ScmConfiguration configuration; private final ScmConfiguration configuration;
@@ -66,7 +66,7 @@ public final class AccessTokenCookieIssuer {
* @param configuration scm main configuration * @param configuration scm main configuration
*/ */
@Inject @Inject
public AccessTokenCookieIssuer(ScmConfiguration configuration) { public DefaultAccessTokenCookieIssuer(ScmConfiguration configuration) {
this.configuration = configuration; this.configuration = configuration;
} }

View File

@@ -87,6 +87,7 @@ public final class JwtAccessToken implements AccessToken {
return ofNullable(claims.get(REFRESHABLE_UNTIL_CLAIM_KEY, Date.class)); return ofNullable(claims.get(REFRESHABLE_UNTIL_CLAIM_KEY, Date.class));
} }
@Override
public Optional<String> getParentKey() { public Optional<String> getParentKey() {
return ofNullable(claims.get(PARENT_TOKEN_ID_CLAIM_KEY).toString()); return ofNullable(claims.get(PARENT_TOKEN_ID_CLAIM_KEY).toString());
} }

View File

@@ -18,6 +18,7 @@ import sonia.scm.security.AccessToken;
import sonia.scm.security.AccessTokenBuilder; import sonia.scm.security.AccessTokenBuilder;
import sonia.scm.security.AccessTokenBuilderFactory; import sonia.scm.security.AccessTokenBuilderFactory;
import sonia.scm.security.AccessTokenCookieIssuer; import sonia.scm.security.AccessTokenCookieIssuer;
import sonia.scm.security.DefaultAccessTokenCookieIssuer;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
@@ -46,7 +47,7 @@ public class AuthenticationResourceTest {
@Mock @Mock
private AccessTokenBuilder accessTokenBuilder; private AccessTokenBuilder accessTokenBuilder;
private AccessTokenCookieIssuer cookieIssuer = new AccessTokenCookieIssuer(mock(ScmConfiguration.class)); private AccessTokenCookieIssuer cookieIssuer = new DefaultAccessTokenCookieIssuer(mock(ScmConfiguration.class));
private static final String AUTH_JSON_TRILLIAN = "{\n" + private static final String AUTH_JSON_TRILLIAN = "{\n" +
"\t\"cookie\": true,\n" + "\t\"cookie\": true,\n" +

View File

@@ -4,9 +4,9 @@ import org.jboss.resteasy.core.Dispatcher;
import org.jboss.resteasy.mock.MockDispatcherFactory; import org.jboss.resteasy.mock.MockDispatcherFactory;
import sonia.scm.api.rest.AlreadyExistsExceptionMapper; import sonia.scm.api.rest.AlreadyExistsExceptionMapper;
import sonia.scm.api.rest.AuthorizationExceptionMapper; import sonia.scm.api.rest.AuthorizationExceptionMapper;
import sonia.scm.api.rest.BadRequestExceptionMapper;
import sonia.scm.api.rest.ConcurrentModificationExceptionMapper; import sonia.scm.api.rest.ConcurrentModificationExceptionMapper;
import sonia.scm.api.v2.NotFoundExceptionMapper; import sonia.scm.api.v2.NotFoundExceptionMapper;
import sonia.scm.api.v2.NotSupportedFeatureExceptionMapper;
public class DispatcherMock { public class DispatcherMock {
public static Dispatcher createDispatcher(Object resource) { public static Dispatcher createDispatcher(Object resource) {
@@ -18,9 +18,7 @@ public class DispatcherMock {
dispatcher.getProviderFactory().register(new ConcurrentModificationExceptionMapper(mapper)); dispatcher.getProviderFactory().register(new ConcurrentModificationExceptionMapper(mapper));
dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class); dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class);
dispatcher.getProviderFactory().register(new InternalRepositoryExceptionMapper(mapper)); dispatcher.getProviderFactory().register(new InternalRepositoryExceptionMapper(mapper));
dispatcher.getProviderFactory().register(new ChangePasswordNotAllowedExceptionMapper(mapper)); dispatcher.getProviderFactory().register(new BadRequestExceptionMapper(mapper));
dispatcher.getProviderFactory().register(new InvalidPasswordExceptionMapper(mapper));
dispatcher.getProviderFactory().register(new NotSupportedFeatureExceptionMapper(mapper));
return dispatcher; return dispatcher;
} }
} }

View File

@@ -20,11 +20,11 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
public class AccessTokenCookieIssuerTest { public class DefaultAccessTokenCookieIssuerTest {
private ScmConfiguration configuration; private ScmConfiguration configuration;
private AccessTokenCookieIssuer issuer; private DefaultAccessTokenCookieIssuer issuer;
@Mock @Mock
private HttpServletRequest request; private HttpServletRequest request;
@@ -41,7 +41,7 @@ public class AccessTokenCookieIssuerTest {
@Before @Before
public void setUp() { public void setUp() {
configuration = new ScmConfiguration(); configuration = new ScmConfiguration();
issuer = new AccessTokenCookieIssuer(configuration); issuer = new DefaultAccessTokenCookieIssuer(configuration);
} }
@Test @Test