diff --git a/Jenkinsfile b/Jenkinsfile index e317ed77d4..f69069d72c 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -107,7 +107,8 @@ void analyzeWith(Maven mvn) { "-Dsonar.pullrequest.key=${env.CHANGE_ID} " + "-Dsonar.pullrequest.provider=bitbucketcloud " + "-Dsonar.pullrequest.bitbucketcloud.owner=sdorra " + - "-Dsonar.pullrequest.bitbucketcloud.repository=scm-manager " + "-Dsonar.pullrequest.bitbucketcloud.repository=scm-manager " + + "-Dsonar.cpd.exclusions=**/*StoreFactory.java,**/*UserPassword.js " } else { mvnArgs += " -Dsonar.branch.name=${env.BRANCH_NAME} " if (!isMainBranch()) { diff --git a/pom.xml b/pom.xml index f8bb7c5727..e04a47ef41 100644 --- a/pom.xml +++ b/pom.xml @@ -810,6 +810,13 @@ SCM-BSD 1.2.0.Final + + + + + + **/*StoreFactory.java,**/*UserPassword.js + diff --git a/scm-core/pom.xml b/scm-core/pom.xml index 3c90fec779..66859a12ee 100644 --- a/scm-core/pom.xml +++ b/scm-core/pom.xml @@ -221,5 +221,5 @@ - + diff --git a/scm-core/src/main/java/sonia/scm/repository/AbstractRepositoryHandler.java b/scm-core/src/main/java/sonia/scm/repository/AbstractRepositoryHandler.java index b13cc0e26b..a3e8a1da73 100644 --- a/scm-core/src/main/java/sonia/scm/repository/AbstractRepositoryHandler.java +++ b/scm-core/src/main/java/sonia/scm/repository/AbstractRepositoryHandler.java @@ -72,9 +72,11 @@ public abstract class AbstractRepositoryHandler * * @param storeFactory */ - protected AbstractRepositoryHandler(ConfigurationStoreFactory storeFactory) - { - this.store = storeFactory.getStore(getConfigClass(), getType().getName()); + protected AbstractRepositoryHandler(ConfigurationStoreFactory storeFactory) { + this.store = storeFactory + .withType(getConfigClass()) + .withName(getType().getName()) + .build(); } //~--- get methods ---------------------------------------------------------- diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java index 922c16c879..63d499deb8 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java @@ -4,7 +4,6 @@ import groovy.lang.Singleton; import sonia.scm.SCMContextProvider; import javax.inject.Inject; -import java.io.File; import java.nio.file.Path; /** diff --git a/scm-core/src/main/java/sonia/scm/security/AccessToken.java b/scm-core/src/main/java/sonia/scm/security/AccessToken.java index 714b09eff8..c2a5f4b747 100644 --- a/scm-core/src/main/java/sonia/scm/security/AccessToken.java +++ b/scm-core/src/main/java/sonia/scm/security/AccessToken.java @@ -31,6 +31,7 @@ package sonia.scm.security; import java.util.Date; +import java.util.Map; import java.util.Optional; /** @@ -38,70 +39,77 @@ import java.util.Optional; * be issued from a restful webservice endpoint by providing credentials. After the token was issued, the token must be * send along with every request. The token should be send in its compact representation as bearer authorization header * or as cookie. - * + * * @author Sebastian Sdorra * @since 2.0.0 */ public interface AccessToken { - + /** * Returns unique id of the access token. - * + * * @return unique id */ String getId(); - + /** * Returns name of subject which identifies the principal. - * + * * @return name of subject */ String getSubject(); - + /** * Returns optional issuer. The issuer identifies the principal that issued the token. - * + * * @return optional issuer */ Optional getIssuer(); - + /** * Returns time at which the token was issued. - * + * * @return time at which the token was issued */ Date getIssuedAt(); - + /** * Returns the expiration time of token. - * + * * @return expiration time */ Date getExpiration(); - + + Optional getRefreshExpiration(); + /** - * 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 * please have a look at {@link Scope}. - * + * * @return scope of token. */ Scope getScope(); - + /** * Returns an optional value of a custom token field. - * + * * @param type of field * @param key key of token field - * + * * @return optional value of custom field */ Optional getCustom(String key); - + /** * Returns compact representation of token. - * + * * @return compact representation */ String compact(); + + /** + * Returns read only map of all claim keys with their values. + */ + Map getClaims(); } diff --git a/scm-core/src/main/java/sonia/scm/security/AccessTokenBuilder.java b/scm-core/src/main/java/sonia/scm/security/AccessTokenBuilder.java index dd7986c22a..5e36ba468f 100644 --- a/scm-core/src/main/java/sonia/scm/security/AccessTokenBuilder.java +++ b/scm-core/src/main/java/sonia/scm/security/AccessTokenBuilder.java @@ -74,11 +74,21 @@ public interface AccessTokenBuilder { * Sets the expiration for the token. * * @param count expiration count - * @param unit expirtation unit + * @param unit expiration unit * * @return {@code this} */ AccessTokenBuilder expiresIn(long count, TimeUnit unit); + + /** + * Sets the time how long this token may be refreshed. Set this to 0 (zero) to disable automatic refresh. + * + * @param count Time unit count. If set to 0, automatic refresh is disabled. + * @param unit time unit + * + * @return {@code this} + */ + AccessTokenBuilder refreshableFor(long count, TimeUnit unit); /** * Reduces the permissions of the token by providing a scope. diff --git a/scm-core/src/main/java/sonia/scm/store/BlobStoreFactory.java b/scm-core/src/main/java/sonia/scm/store/BlobStoreFactory.java index 941b3923d1..cf58fc43c7 100644 --- a/scm-core/src/main/java/sonia/scm/store/BlobStoreFactory.java +++ b/scm-core/src/main/java/sonia/scm/store/BlobStoreFactory.java @@ -32,9 +32,25 @@ package sonia.scm.store; +import sonia.scm.repository.Repository; + /** - * The BlobStoreFactory can be used to create new or get existing - * {@link BlobStore}s. + * The BlobStoreFactory can be used to create a new or get an existing {@link BlobStore}s. + *
+ * You can either create a global {@link BlobStore} or a {@link BlobStore} for a specific repository. To create a global + * {@link BlobStore} call: + *
+ *     blobStoreFactory
+ *       .withName("name")
+ *       .build();
+ * 
+ * To create a {@link BlobStore} for a specific repository call: + *
+ *     blobStoreFactory
+ *       .withName("name")
+ *       .forRepository(repository)
+ *       .build();
+ * 
* * @author Sebastian Sdorra * @since 1.23 @@ -45,13 +61,68 @@ package sonia.scm.store; public interface BlobStoreFactory { /** - * Returns a {@link BlobStore} with the given name, if the {@link BlobStore} - * with the given name does not exists the factory will create a new one. + * Creates a new or gets an existing {@link BlobStore}. Instead of calling this method you should use the floating API + * from {@link #withName(String)}. * - * - * @param name name of the {@link BlobStore} - * - * @return {@link BlobStore} with the given name + * @param storeParameters The parameters for the blob store. + * @return A new or an existing {@link BlobStore} for the given parameters. */ - public BlobStore getBlobStore(String name); + BlobStore getStore(final StoreParameters storeParameters); + + /** + * Use this to create a new or get an existing {@link BlobStore} with a floating API. + * @param name The name for the {@link BlobStore}. + * @return Floating API to either specify a repository or directly build a global {@link BlobStore}. + */ + default FloatingStoreParameters.Builder withName(String name) { + return new FloatingStoreParameters(this).new Builder(name); + } +} + +final class FloatingStoreParameters implements StoreParameters { + + private String name; + private Repository repository; + + private final BlobStoreFactory factory; + + FloatingStoreParameters(BlobStoreFactory factory) { + this.factory = factory; + } + + @Override + public String getName() { + return name; + } + + @Override + public Repository getRepository() { + return repository; + } + + public class Builder { + + Builder(String name) { + FloatingStoreParameters.this.name = name; + } + + /** + * Use this to create or get a {@link BlobStore} for a specific repository. This step is optional. If you want to + * have a global {@link BlobStore}, omit this. + * @param repository The optional repository for the {@link BlobStore}. + * @return Floating API to finish the call. + */ + public FloatingStoreParameters.Builder forRepository(Repository repository) { + FloatingStoreParameters.this.repository = repository; + return this; + } + + /** + * Creates or gets the {@link BlobStore} with the given name and (if specified) the given repository. If no + * repository is given, the {@link BlobStore} will be global. + */ + public BlobStore build(){ + return factory.getStore(FloatingStoreParameters.this); + } + } } diff --git a/scm-core/src/main/java/sonia/scm/store/ConfigurationEntryStoreFactory.java b/scm-core/src/main/java/sonia/scm/store/ConfigurationEntryStoreFactory.java index 7cfebd69c1..80f9cb3df9 100644 --- a/scm-core/src/main/java/sonia/scm/store/ConfigurationEntryStoreFactory.java +++ b/scm-core/src/main/java/sonia/scm/store/ConfigurationEntryStoreFactory.java @@ -32,31 +32,104 @@ package sonia.scm.store; +import sonia.scm.repository.Repository; + /** - * The ConfigurationEntryStoreFactory can be used to create new or get existing - * {@link ConfigurationEntryStore}s. Note: the default implementation - * uses the same location as the {@link StoreFactory}, so be sure that the - * store names are unique for all {@link ConfigurationEntryStore}s and - * {@link Store}s. - * + * The ConfigurationEntryStoreFactory can be used to create new or get existing {@link ConfigurationEntryStore}s. + *
+ * Note: the default implementation uses the same location as the {@link ConfigurationStoreFactory}, so be sure + * that the store names are unique for all {@link ConfigurationEntryStore}s and {@link ConfigurationEntryStore}s. + *
+ * You can either create a global {@link ConfigurationEntryStore} or a {@link ConfigurationEntryStore} for a specific + * repository. To create a global {@link ConfigurationEntryStore} call: + *
+ *     configurationEntryStoreFactory
+ *       .withType(PersistedType.class)
+ *       .withName("name")
+ *       .build();
+ * 
+ * To create a {@link ConfigurationEntryStore} for a specific repository call: + *
+ *     configurationEntryStoreFactory
+ *       .withType(PersistedType.class)
+ *       .withName("name")
+ *       .forRepository(repository)
+ *       .build();
+ * 
+ * * @author Sebastian Sdorra * @since 1.31 * * @apiviz.landmark * @apiviz.uses sonia.scm.store.ConfigurationEntryStore */ -public interface ConfigurationEntryStoreFactory -{ +public interface ConfigurationEntryStoreFactory { /** - * Get an existing {@link ConfigurationEntryStore} or create a new one. + * Creates a new or gets an existing {@link ConfigurationEntryStore}. Instead of calling this method you should use + * the floating API from {@link #withType(Class)}. * - * - * @param type type of the store objects - * @param name name of the store - * @param type of the store objects - * - * @return {@link ConfigurationEntryStore} with given name and type + * @param storeParameters The parameters for the {@link ConfigurationEntryStore}. + * @return A new or an existing {@link ConfigurationEntryStore} for the given parameters. */ - public ConfigurationEntryStore getStore(Class type, String name); + ConfigurationEntryStore getStore(final TypedStoreParameters storeParameters); + + /** + * Use this to create a new or get an existing {@link ConfigurationEntryStore} with a floating API. + * @param type The type for the {@link ConfigurationEntryStore}. + * @return Floating API to set the name and either specify a repository or directly build a global + * {@link ConfigurationEntryStore}. + */ + default TypedFloatingConfigurationEntryStoreParameters.Builder withType(Class type) { + return new TypedFloatingConfigurationEntryStoreParameters(this).new Builder(type); + } +} + +final class TypedFloatingConfigurationEntryStoreParameters { + + private final TypedStoreParametersImpl parameters = new TypedStoreParametersImpl<>(); + private final ConfigurationEntryStoreFactory factory; + + TypedFloatingConfigurationEntryStoreParameters(ConfigurationEntryStoreFactory factory) { + this.factory = factory; + } + + public class Builder { + + Builder(Class type) { + parameters.setType(type); + } + + /** + * Use this to set the name for the {@link ConfigurationEntryStore}. + * @param name The name for the {@link ConfigurationEntryStore}. + * @return Floating API to either specify a repository or directly build a global {@link ConfigurationEntryStore}. + */ + public OptionalRepositoryBuilder withName(String name) { + parameters.setName(name); + return new OptionalRepositoryBuilder(); + } + } + + public class OptionalRepositoryBuilder { + + /** + * Use this to create or get a {@link ConfigurationEntryStore} for a specific repository. This step is optional. If + * you want to have a global {@link ConfigurationEntryStore}, omit this. + * @param repository The optional repository for the {@link ConfigurationEntryStore}. + * @return Floating API to finish the call. + */ + public OptionalRepositoryBuilder forRepository(Repository repository) { + parameters.setRepository(repository); + return this; + } + + /** + * Creates or gets the {@link ConfigurationEntryStore} with the given name and (if specified) the given repository. + * If no repository is given, the {@link ConfigurationEntryStore} will be global. + */ + public ConfigurationEntryStore build(){ + return factory.getStore(parameters); + } + } } diff --git a/scm-core/src/main/java/sonia/scm/store/ConfigurationStoreFactory.java b/scm-core/src/main/java/sonia/scm/store/ConfigurationStoreFactory.java index d9a97de98d..6624f307e7 100644 --- a/scm-core/src/main/java/sonia/scm/store/ConfigurationStoreFactory.java +++ b/scm-core/src/main/java/sonia/scm/store/ConfigurationStoreFactory.java @@ -33,27 +33,103 @@ package sonia.scm.store; +import sonia.scm.repository.Repository; + /** - * The ConfigurationStoreFactory can be used to create new or get existing - * {@link ConfigurationStore} objects. + * The ConfigurationStoreFactory can be used to create new or get existing {@link ConfigurationStore} objects. + *
+ * Note: the default implementation uses the same location as the {@link ConfigurationEntryStoreFactory}, so be + * sure that the store names are unique for all {@link ConfigurationEntryStore}s and {@link ConfigurationStore}s. + *
+ * You can either create a global {@link ConfigurationStore} or a {@link ConfigurationStore} for a specific repository. + * To create a global {@link ConfigurationStore} call: + *
+ *     configurationStoreFactory
+ *       .withType(PersistedType.class)
+ *       .withName("name")
+ *       .build();
+ * 
+ * To create a {@link ConfigurationStore} for a specific repository call: + *
+ *     configurationStoreFactory
+ *       .withType(PersistedType.class)
+ *       .withName("name")
+ *       .forRepository(repository)
+ *       .build();
+ * 
* * @author Sebastian Sdorra * * @apiviz.landmark * @apiviz.uses sonia.scm.store.ConfigurationStore */ -public interface ConfigurationStoreFactory -{ +public interface ConfigurationStoreFactory { /** - * Get an existing {@link ConfigurationStore} or create a new one. + * Creates a new or gets an existing {@link ConfigurationStore}. Instead of calling this method you should use the + * floating API from {@link #withType(Class)}. * - * - * @param type type of the store objects - * @param name name of the store - * @param type of the store objects - * - * @return {@link ConfigurationStore} of the given type and name + * @param storeParameters The parameters for the {@link ConfigurationStore}. + * @return A new or an existing {@link ConfigurationStore} for the given parameters. */ - public ConfigurationStore getStore(Class type, String name); + ConfigurationStore getStore(final TypedStoreParameters storeParameters); + + /** + * Use this to create a new or get an existing {@link ConfigurationStore} with a floating API. + * @param type The type for the {@link ConfigurationStore}. + * @return Floating API to set the name and either specify a repository or directly build a global + * {@link ConfigurationStore}. + */ + default TypedFloatingConfigurationStoreParameters.Builder withType(Class type) { + return new TypedFloatingConfigurationStoreParameters(this).new Builder(type); + } +} + +final class TypedFloatingConfigurationStoreParameters { + + private final TypedStoreParametersImpl parameters = new TypedStoreParametersImpl<>(); + private final ConfigurationStoreFactory factory; + + TypedFloatingConfigurationStoreParameters(ConfigurationStoreFactory factory) { + this.factory = factory; + } + + public class Builder { + + Builder(Class type) { + parameters.setType(type); + } + + /** + * Use this to set the name for the {@link ConfigurationStore}. + * @param name The name for the {@link ConfigurationStore}. + * @return Floating API to either specify a repository or directly build a global {@link ConfigurationStore}. + */ + public OptionalRepositoryBuilder withName(String name) { + parameters.setName(name); + return new OptionalRepositoryBuilder(); + } + } + + public class OptionalRepositoryBuilder { + + /** + * Use this to create or get a {@link ConfigurationStore} for a specific repository. This step is optional. If you + * want to have a global {@link ConfigurationStore}, omit this. + * @param repository The optional repository for the {@link ConfigurationStore}. + * @return Floating API to finish the call. + */ + public OptionalRepositoryBuilder forRepository(Repository repository) { + parameters.setRepository(repository); + return this; + } + + /** + * Creates or gets the {@link ConfigurationStore} with the given name and (if specified) the given repository. If no + * repository is given, the {@link ConfigurationStore} will be global. + */ + public ConfigurationStore build(){ + return factory.getStore(parameters); + } + } } diff --git a/scm-core/src/main/java/sonia/scm/store/DataStoreFactory.java b/scm-core/src/main/java/sonia/scm/store/DataStoreFactory.java index caed974ee4..564c339d3d 100644 --- a/scm-core/src/main/java/sonia/scm/store/DataStoreFactory.java +++ b/scm-core/src/main/java/sonia/scm/store/DataStoreFactory.java @@ -32,9 +32,27 @@ package sonia.scm.store; +import sonia.scm.repository.Repository; + /** - * The DataStoreFactory can be used to create new or get existing - * {@link DataStore}s. + * The DataStoreFactory can be used to create new or get existing {@link DataStore}s. + *
+ * You can either create a global {@link DataStore} or a {@link DataStore} for a specific repository. + * To create a global {@link DataStore} call: + *
+ *     dataStoreFactory
+ *       .withType(PersistedType.class)
+ *       .withName("name")
+ *       .build();
+ * 
+ * To create a {@link DataStore} for a specific repository call: + *
+ *     dataStoreFactory
+ *       .withType(PersistedType.class)
+ *       .withName("name")
+ *       .forRepository(repository)
+ *       .build();
+ * 
* * @author Sebastian Sdorra * @since 1.23 @@ -45,14 +63,70 @@ package sonia.scm.store; public interface DataStoreFactory { /** - * Get an existing {@link DataStore} or create a new one. + * Creates a new or gets an existing {@link DataStore}. Instead of calling this method you should use the + * floating API from {@link #withType(Class)}. * - * - * @param type type of the store objects - * @param name name of the store - * @param type of the store objects - * - * @return {@link DataStore} with given name and type + * @param storeParameters The parameters for the {@link DataStore}. + * @return A new or an existing {@link DataStore} for the given parameters. */ - public DataStore getStore(Class type, String name); + DataStore getStore(final TypedStoreParameters storeParameters); + + /** + * Use this to create a new or get an existing {@link DataStore} with a floating API. + * @param type The type for the {@link DataStore}. + * @return Floating API to set the name and either specify a repository or directly build a global + * {@link DataStore}. + */ + default TypedFloatingDataStoreParameters.Builder withType(Class type) { + return new TypedFloatingDataStoreParameters(this).new Builder(type); + } +} + +final class TypedFloatingDataStoreParameters { + + private final TypedStoreParametersImpl parameters = new TypedStoreParametersImpl<>(); + private final DataStoreFactory factory; + + TypedFloatingDataStoreParameters(DataStoreFactory factory) { + this.factory = factory; + } + + public class Builder { + + Builder(Class type) { + parameters.setType(type); + } + + /** + * Use this to set the name for the {@link DataStore}. + * @param name The name for the {@link DataStore}. + * @return Floating API to either specify a repository or directly build a global {@link DataStore}. + */ + public OptionalRepositoryBuilder withName(String name) { + parameters.setName(name); + return new OptionalRepositoryBuilder(); + } + } + + public class OptionalRepositoryBuilder { + + /** + * Use this to create or get a {@link DataStore} for a specific repository. This step is optional. If you + * want to have a global {@link DataStore}, omit this. + * @param repository The optional repository for the {@link DataStore}. + * @return Floating API to finish the call. + */ + public OptionalRepositoryBuilder forRepository(Repository repository) { + parameters.setRepository(repository); + return this; + } + + /** + * Creates or gets the {@link DataStore} with the given name and (if specified) the given repository. If no + * repository is given, the {@link DataStore} will be global. + */ + public DataStore build(){ + return factory.getStore(parameters); + } + } } diff --git a/scm-core/src/main/java/sonia/scm/store/StoreParameters.java b/scm-core/src/main/java/sonia/scm/store/StoreParameters.java new file mode 100644 index 0000000000..da8ee4c916 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/store/StoreParameters.java @@ -0,0 +1,16 @@ +package sonia.scm.store; + +import sonia.scm.repository.Repository; + +/** + * The fields of the {@link StoreParameters} are used from the {@link BlobStoreFactory} to create a store. + * + * @author Mohamed Karray + * @since 2.0.0 + */ +public interface StoreParameters { + + String getName(); + + Repository getRepository(); +} diff --git a/scm-core/src/main/java/sonia/scm/store/TypedStoreParameters.java b/scm-core/src/main/java/sonia/scm/store/TypedStoreParameters.java new file mode 100644 index 0000000000..116bccac41 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/store/TypedStoreParameters.java @@ -0,0 +1,19 @@ +package sonia.scm.store; + +import sonia.scm.repository.Repository; + +/** + * The fields of the {@link TypedStoreParameters} are used from the {@link ConfigurationStoreFactory}, + * {@link ConfigurationEntryStoreFactory} and {@link DataStoreFactory} to create a type safe store. + * + * @author Mohamed Karray + * @since 2.0.0 + */ +public interface TypedStoreParameters { + + Class getType(); + + String getName(); + + Repository getRepository(); +} diff --git a/scm-core/src/main/java/sonia/scm/store/TypedStoreParametersImpl.java b/scm-core/src/main/java/sonia/scm/store/TypedStoreParametersImpl.java new file mode 100644 index 0000000000..50ce6a496b --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/store/TypedStoreParametersImpl.java @@ -0,0 +1,36 @@ +package sonia.scm.store; + +import sonia.scm.repository.Repository; + +class TypedStoreParametersImpl implements TypedStoreParameters { + private Class type; + private String name; + private Repository repository; + + @Override + public Class getType() { + return type; + } + + void setType(Class type) { + this.type = type; + } + + @Override + public String getName() { + return name; + } + + void setName(String name) { + this.name = name; + } + + @Override + public Repository getRepository() { + return repository; + } + + void setRepository(Repository repository) { + this.repository = repository; + } +} diff --git a/scm-dao-xml/src/main/java/sonia/scm/group/xml/XmlGroupDAO.java b/scm-dao-xml/src/main/java/sonia/scm/group/xml/XmlGroupDAO.java index 577d732317..d6b65b41bd 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/group/xml/XmlGroupDAO.java +++ b/scm-dao-xml/src/main/java/sonia/scm/group/xml/XmlGroupDAO.java @@ -64,9 +64,11 @@ public class XmlGroupDAO extends AbstractXmlDAO * @param storeFactory */ @Inject - public XmlGroupDAO(ConfigurationStoreFactory storeFactory) - { - super(storeFactory.getStore(XmlGroupDatabase.class, STORE_NAME)); + public XmlGroupDAO(ConfigurationStoreFactory storeFactory) { + super(storeFactory + .withType(XmlGroupDatabase.class) + .withName(STORE_NAME) + .build()); } //~--- methods -------------------------------------------------------------- diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreFactory.java index 5bfc4f34b9..099ab53baa 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreFactory.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreFactory.java @@ -31,18 +31,21 @@ package sonia.scm.store; //~--- non-JDK imports -------------------------------------------------------- + import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.SCMContextProvider; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryLocationResolver; import sonia.scm.util.IOUtil; -//~--- JDK imports ------------------------------------------------------------ import java.io.File; +//~--- JDK imports ------------------------------------------------------------ + /** * Abstract store factory for file based stores. - * + * * @author Sebastian Sdorra */ public abstract class FileBasedStoreFactory { @@ -51,39 +54,57 @@ public abstract class FileBasedStoreFactory { * the logger for FileBasedStoreFactory */ private static final Logger LOG = LoggerFactory.getLogger(FileBasedStoreFactory.class); + private SCMContextProvider contextProvider; + private RepositoryLocationResolver repositoryLocationResolver; + private Store store; - private static final String BASE_DIRECTORY = "var"; + private File storeDirectory; - private final SCMContextProvider context; - - private final String dataDirectoryName; - - private File dataDirectory; - - protected FileBasedStoreFactory(SCMContextProvider context, - String dataDirectoryName) { - this.context = context; - this.dataDirectoryName = dataDirectoryName; + protected FileBasedStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, Store store) { + this.contextProvider = contextProvider; + this.repositoryLocationResolver = repositoryLocationResolver; + this.store = store; } - //~--- get methods ---------------------------------------------------------- - /** - * Returns data directory for given name. - * - * @param name name of data directory - * - * @return data directory - */ - protected File getDirectory(String name) { - if (dataDirectory == null) { - dataDirectory = new File(context.getBaseDirectory(), - BASE_DIRECTORY.concat(File.separator).concat(dataDirectoryName)); - LOG.debug("create data directory {}", dataDirectory); - } + protected File getStoreLocation(StoreParameters storeParameters) { + return getStoreLocation(storeParameters.getName(), null, storeParameters.getRepository()); + } - File storeDirectory = new File(dataDirectory, name); - IOUtil.mkdirs(storeDirectory); - return storeDirectory; + protected File getStoreLocation(TypedStoreParameters storeParameters) { + return getStoreLocation(storeParameters.getName(), storeParameters.getType(), storeParameters.getRepository()); + } + + protected File getStoreLocation(String name, Class type, Repository repository) { + if (storeDirectory == null) { + if (repository != null) { + LOG.debug("create store with type: {}, name: {} and repository: {}", type, name, repository.getNamespaceAndName()); + storeDirectory = this.getStoreDirectory(store, repository); + } else { + LOG.debug("create store with type: {} and name: {} ", type, name); + storeDirectory = this.getStoreDirectory(store); + } + IOUtil.mkdirs(storeDirectory); + } + return new File(this.storeDirectory, name); + } + + /** + * Get the store directory of a specific repository + * @param store the type of the store + * @param repository the repo + * @return the store directory of a specific repository + */ + private File getStoreDirectory(Store store, Repository repository) { + return new File(repositoryLocationResolver.getPath(repository.getId()).toFile(), store.getRepositoryStoreDirectory()); + } + + /** + * Get the global store directory + * @param store the type of the store + * @return the global store directory + */ + private File getStoreDirectory(Store store) { + return new File(contextProvider.getBaseDirectory(), store.getGlobalStoreDirectory()); } } diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/FileBlobStoreFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/FileBlobStoreFactory.java index 8cc5b34ac2..7e2e5a9e29 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/FileBlobStoreFactory.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/FileBlobStoreFactory.java @@ -31,14 +31,17 @@ package sonia.scm.store; //~--- non-JDK imports -------------------------------------------------------- + import com.google.inject.Inject; import com.google.inject.Singleton; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.SCMContextProvider; +import sonia.scm.repository.RepositoryLocationResolver; import sonia.scm.security.KeyGenerator; +import sonia.scm.util.IOUtil; + +import java.io.File; /** * File based store factory. @@ -48,8 +51,6 @@ import sonia.scm.security.KeyGenerator; @Singleton public class FileBlobStoreFactory extends FileBasedStoreFactory implements BlobStoreFactory { - private static final String DIRECTORY_NAME = "blob"; - /** * the logger for FileBlobStoreFactory */ @@ -60,21 +61,22 @@ public class FileBlobStoreFactory extends FileBasedStoreFactory implements BlobS /** * Constructs a new instance. * - * @param context scm context + * @param repositoryLocationResolver location resolver * @param keyGenerator key generator */ @Inject - public FileBlobStoreFactory(SCMContextProvider context, - KeyGenerator keyGenerator) { - super(context, DIRECTORY_NAME); + public FileBlobStoreFactory(SCMContextProvider contextProvider ,RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator) { + super(contextProvider, repositoryLocationResolver, Store.BLOB); this.keyGenerator = keyGenerator; } @Override - public BlobStore getBlobStore(String name) { - LOG.debug("create new blob with name {}", name); - - return new FileBlobStore(keyGenerator, getDirectory(name)); + @SuppressWarnings("unchecked") + public BlobStore getStore(StoreParameters storeParameters) { + File storeLocation = getStoreLocation(storeParameters); + IOUtil.mkdirs(storeLocation); + return new FileBlobStore(keyGenerator, storeLocation); } + } diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStoreFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStoreFactory.java index 261c36f8e4..96403140ef 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStoreFactory.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStoreFactory.java @@ -1,19 +1,19 @@ /** * Copyright (c) 2010, Sebastian Sdorra * All rights reserved. - * + *

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

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

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

* http://bitbucket.org/sdorra/scm-manager - * */ - package sonia.scm.store; //~--- non-JDK imports -------------------------------------------------------- import com.google.inject.Inject; import com.google.inject.Singleton; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import sonia.scm.SCMContextProvider; +import sonia.scm.repository.RepositoryLocationResolver; import sonia.scm.security.KeyGenerator; -import sonia.scm.util.IOUtil; //~--- JDK imports ------------------------------------------------------------ -import java.io.File; - /** * * @author Sebastian Sdorra */ @Singleton -public class JAXBConfigurationEntryStoreFactory - implements ConfigurationEntryStoreFactory -{ +public class JAXBConfigurationEntryStoreFactory extends FileBasedStoreFactory + implements ConfigurationEntryStoreFactory { - /** - * the logger for JAXBConfigurationEntryStoreFactory - */ - private static final Logger logger = - LoggerFactory.getLogger(JAXBConfigurationEntryStoreFactory.class); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param keyGenerator - * @param context - */ - @Inject - public JAXBConfigurationEntryStoreFactory(KeyGenerator keyGenerator, - SCMContextProvider context) - { - this.keyGenerator = keyGenerator; - directory = new File(context.getBaseDirectory(), - StoreConstants.CONFIG_DIRECTORY_NAME); - IOUtil.mkdirs(directory); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param type - * @param name - * @param - * - * @return - */ - @Override - public ConfigurationEntryStore getStore(Class type, String name) - { - logger.debug("create new configuration store for type {} with name {}", - type, name); - - //J- - return new JAXBConfigurationEntryStore( - new File(directory,name.concat(StoreConstants.FILE_EXTENSION)), - keyGenerator, - type - ); - //J+ - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private File directory; - - /** Field description */ private KeyGenerator keyGenerator; + + @Inject + public JAXBConfigurationEntryStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator) { + super(contextProvider, repositoryLocationResolver, Store.CONFIG); + this.keyGenerator = keyGenerator; + } + + @Override + public ConfigurationEntryStore getStore(TypedStoreParameters storeParameters) { + return new JAXBConfigurationEntryStore<>(getStoreLocation(storeParameters.getName().concat(StoreConstants.FILE_EXTENSION), storeParameters.getType(), storeParameters.getRepository()), keyGenerator, storeParameters.getType()); + } + } diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStore.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStore.java index 4a87ea57f6..ac1477d7ea 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStore.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStore.java @@ -61,7 +61,7 @@ public class JAXBConfigurationStore extends AbstractStore { private JAXBContext context; - JAXBConfigurationStore(Class type, File configFile) { + public JAXBConfigurationStore(Class type, File configFile) { this.type = type; try { diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStoreFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStoreFactory.java index 705b0c1177..bb68ab93dc 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStoreFactory.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStoreFactory.java @@ -32,14 +32,8 @@ package sonia.scm.store; import com.google.inject.Inject; import com.google.inject.Singleton; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import sonia.scm.SCMContextProvider; -import sonia.scm.util.IOUtil; - -import java.io.File; +import sonia.scm.repository.RepositoryLocationResolver; /** * JAXB implementation of {@link ConfigurationStoreFactory}. @@ -47,40 +41,20 @@ import java.io.File; * @author Sebastian Sdorra */ @Singleton -public class JAXBConfigurationStoreFactory implements ConfigurationStoreFactory { - - /** - * the logger for JAXBConfigurationStoreFactory - */ - private static final Logger LOG = LoggerFactory.getLogger(JAXBConfigurationStoreFactory.class); - - private final File configDirectory; +public class JAXBConfigurationStoreFactory extends FileBasedStoreFactory implements ConfigurationStoreFactory { /** * Constructs a new instance. * - * @param context scm context + * @param repositoryLocationResolver Resolver to get the repository Directory */ @Inject - public JAXBConfigurationStoreFactory(SCMContextProvider context) { - configDirectory = new File(context.getBaseDirectory(), StoreConstants.CONFIG_DIRECTORY_NAME); - IOUtil.mkdirs(configDirectory); + public JAXBConfigurationStoreFactory(SCMContextProvider contextProvider, RepositoryLocationResolver repositoryLocationResolver) { + super(contextProvider, repositoryLocationResolver, Store.CONFIG); } @Override - public JAXBConfigurationStore getStore(Class type, String name) { - if (configDirectory == null) { - throw new IllegalStateException("store factory is not initialized"); - } - - File configFile = new File(configDirectory, name.concat(StoreConstants.FILE_EXTENSION)); - - if (LOG.isDebugEnabled()) { - LOG.debug("create store for {} at {}", type.getName(), - configFile.getPath()); - } - - return new JAXBConfigurationStore<>(type, configFile); + public JAXBConfigurationStore getStore(TypedStoreParameters storeParameters) { + return new JAXBConfigurationStore<>(storeParameters.getType(), getStoreLocation(storeParameters.getName().concat(StoreConstants.FILE_EXTENSION), storeParameters.getType(), storeParameters.getRepository())); } - } diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBDataStoreFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBDataStoreFactory.java index 732b8c675b..5b5c00a298 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBDataStoreFactory.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBDataStoreFactory.java @@ -41,7 +41,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.SCMContextProvider; +import sonia.scm.repository.RepositoryLocationResolver; import sonia.scm.security.KeyGenerator; +import sonia.scm.util.IOUtil; + +import java.io.File; /** * @@ -49,57 +53,20 @@ import sonia.scm.security.KeyGenerator; */ @Singleton public class JAXBDataStoreFactory extends FileBasedStoreFactory - implements DataStoreFactory -{ + implements DataStoreFactory { - /** Field description */ - private static final String DIRECTORY_NAME = "data"; + private KeyGenerator keyGenerator; - /** - * the logger for JAXBDataStoreFactory - */ - private static final Logger logger = - LoggerFactory.getLogger(JAXBDataStoreFactory.class); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param context - * @param keyGenerator - */ @Inject - public JAXBDataStoreFactory(SCMContextProvider context, - KeyGenerator keyGenerator) - { - super(context, DIRECTORY_NAME); + public JAXBDataStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator) { + super(contextProvider, repositoryLocationResolver, Store.DATA); this.keyGenerator = keyGenerator; } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param type - * @param name - * @param - * - * @return - */ @Override - public DataStore getStore(Class type, String name) - { - logger.debug("create new store for type {} with name {}", type, name); - - return new JAXBDataStore<>(keyGenerator, type, getDirectory(name)); + public DataStore getStore(TypedStoreParameters storeParameters) { + File storeLocation = getStoreLocation(storeParameters); + IOUtil.mkdirs(storeLocation); + return new JAXBDataStore<>(keyGenerator, storeParameters.getType(), storeLocation); } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private KeyGenerator keyGenerator; } diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/Store.java b/scm-dao-xml/src/main/java/sonia/scm/store/Store.java new file mode 100644 index 0000000000..6e5cbcdf65 --- /dev/null +++ b/scm-dao-xml/src/main/java/sonia/scm/store/Store.java @@ -0,0 +1,49 @@ +package sonia.scm.store; + +import java.io.File; + +public enum Store { + CONFIG("config"), + DATA("data"), + BLOB("blob"); + + private static final String GLOBAL_STORE_BASE_DIRECTORY = "var"; + + private String directory; + + Store(String directory) { + + this.directory = directory; + } + + /** + * Get the relkative store directory path to be stored in the repository root + *

+ * The repository store directories are: + * repo_base_dir/config/ + * repo_base_dir/blob/ + * repo_base_dir/data/ + * + * @return the relative store directory path to be stored in the repository root + */ + public String getRepositoryStoreDirectory() { + return directory; + } + + /** + * Get the relative store directory path to be stored in the global root + *

+ * The global store directories are: + * base_dir/config/ + * base_dir/var/blob/ + * base_dir/var/data/ + * + * @return the relative store directory path to be stored in the global root + */ + public String getGlobalStoreDirectory() { + if (this.equals(CONFIG)) { + return directory; + } + return GLOBAL_STORE_BASE_DIRECTORY + File.separator + directory; + } +} diff --git a/scm-dao-xml/src/main/java/sonia/scm/user/xml/XmlUserDAO.java b/scm-dao-xml/src/main/java/sonia/scm/user/xml/XmlUserDAO.java index 1bfd877f44..ea7f18fbba 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/user/xml/XmlUserDAO.java +++ b/scm-dao-xml/src/main/java/sonia/scm/user/xml/XmlUserDAO.java @@ -36,11 +36,10 @@ package sonia.scm.user.xml; import com.google.inject.Inject; import com.google.inject.Singleton; - +import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.user.User; import sonia.scm.user.UserDAO; import sonia.scm.xml.AbstractXmlDAO; -import sonia.scm.store.ConfigurationStoreFactory; /** * @@ -65,7 +64,10 @@ public class XmlUserDAO extends AbstractXmlDAO @Inject public XmlUserDAO(ConfigurationStoreFactory storeFactory) { - super(storeFactory.getStore(XmlUserDatabase.class, STORE_NAME)); + super(storeFactory + .withType(XmlUserDatabase.class) + .withName(STORE_NAME) + .build()); } //~--- methods -------------------------------------------------------------- diff --git a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java index 3910f59bcc..6330db56a0 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java @@ -28,7 +28,8 @@ import java.util.concurrent.atomic.AtomicLong; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; @ExtendWith({MockitoExtension.class, TempDirectory.class}) @MockitoSettings(strictness = Strictness.LENIENT) diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/FileBlobStoreTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/FileBlobStoreTest.java index cae872538d..3ec16baa57 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/store/FileBlobStoreTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/store/FileBlobStoreTest.java @@ -34,8 +34,15 @@ package sonia.scm.store; //~--- non-JDK imports -------------------------------------------------------- +import org.junit.Test; +import sonia.scm.repository.Repository; import sonia.scm.security.UUIDKeyGenerator; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertNotNull; + /** * * @author Sebastian Sdorra @@ -52,6 +59,24 @@ public class FileBlobStoreTest extends BlobStoreTestBase @Override protected BlobStoreFactory createBlobStoreFactory() { - return new FileBlobStoreFactory(contextProvider, new UUIDKeyGenerator()); + return new FileBlobStoreFactory(contextProvider, repositoryLocationResolver, new UUIDKeyGenerator()); + } + + @Test + @SuppressWarnings("unchecked") + public void shouldStoreAndLoadInRepository() { + BlobStore store = createBlobStoreFactory() + .withName("test") + .forRepository(new Repository("id", "git", "ns", "n")) + .build(); + + Blob createdBlob = store.create("abc"); + List storedBlobs = store.getAll(); + + assertNotNull(createdBlob); + assertThat(storedBlobs) + .isNotNull() + .hasSize(1) + .usingElementComparatorOnFields("id").containsExactly(createdBlob); } } diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationEntryStoreTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationEntryStoreTest.java index d0f17fc313..ae84f9d768 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationEntryStoreTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationEntryStoreTest.java @@ -37,25 +37,22 @@ package sonia.scm.store; import com.google.common.io.Closeables; import com.google.common.io.Resources; - import org.junit.Test; - import sonia.scm.security.AssignedPermission; import sonia.scm.security.UUIDKeyGenerator; -import static org.junit.Assert.*; - -//~--- JDK imports ------------------------------------------------------------ - import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; - import java.net.URL; - import java.util.UUID; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -132,13 +129,13 @@ public class JAXBConfigurationEntryStoreTest public void testStoreAndLoad() throws IOException { String name = UUID.randomUUID().toString(); - ConfigurationEntryStore store = - createPermissionStore(RESOURCE_FIXED, name); + ConfigurationEntryStore store = createPermissionStore(RESOURCE_FIXED, name); store.put("a45", new AssignedPermission("tuser4", "repository:create")); - store = - createConfigurationStoreFactory().getStore(AssignedPermission.class, - name); + store = createConfigurationStoreFactory() + .withType(AssignedPermission.class) + .withName(name) + .build(); AssignedPermission ap = store.get("a45"); @@ -147,6 +144,16 @@ public class JAXBConfigurationEntryStoreTest assertEquals("repository:create", ap.getPermission()); } + @Test + public void shouldStoreAndLoadInRepository() throws IOException + { + repoStore.put("abc", new StoreObject("abc_value")); + StoreObject storeObject = repoStore.get("abc"); + + assertNotNull(storeObject); + assertEquals("abc_value", storeObject.getValue()); + } + /** * Method description * @@ -154,10 +161,9 @@ public class JAXBConfigurationEntryStoreTest * @return */ @Override - protected ConfigurationEntryStoreFactory createConfigurationStoreFactory() + protected ConfigurationEntryStoreFactory createConfigurationStoreFactory() { - return new JAXBConfigurationEntryStoreFactory(new UUIDKeyGenerator(), - contextProvider); + return new JAXBConfigurationEntryStoreFactory(contextProvider, repositoryLocationResolver, new UUIDKeyGenerator()); } /** @@ -225,8 +231,9 @@ public class JAXBConfigurationEntryStoreTest } copy(resource, name); - - return createConfigurationStoreFactory().getStore(AssignedPermission.class, - name); + return createConfigurationStoreFactory() + .withType(AssignedPermission.class) + .withName(name) + .build(); } } diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationStoreTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationStoreTest.java index 4151a6ca20..802f193340 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationStoreTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationStoreTest.java @@ -32,9 +32,15 @@ package sonia.scm.store; +import org.junit.Test; +import sonia.scm.repository.Repository; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + /** * Unit tests for {@link JAXBConfigurationStore}. - * + * * @author Sebastian Sdorra */ public class JAXBConfigurationStoreTest extends StoreTestBase { @@ -42,6 +48,24 @@ public class JAXBConfigurationStoreTest extends StoreTestBase { @Override protected ConfigurationStoreFactory createStoreFactory() { - return new JAXBConfigurationStoreFactory(contextProvider); + return new JAXBConfigurationStoreFactory(contextProvider, repositoryLocationResolver); + } + + + @Test + @SuppressWarnings("unchecked") + public void shouldStoreAndLoadInRepository() + { + ConfigurationStore store = createStoreFactory() + .withType(StoreObject.class) + .withName("test") + .forRepository(new Repository("id", "git", "ns", "n")) + .build(); + + store.set(new StoreObject("value")); + StoreObject storeObject = store.get(); + + assertNotNull(storeObject); + assertEquals("value", storeObject.getValue()); } } diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBDataStoreTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBDataStoreTest.java index 9834a48916..04d86aa625 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBDataStoreTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBDataStoreTest.java @@ -34,14 +34,18 @@ package sonia.scm.store; //~--- non-JDK imports -------------------------------------------------------- +import org.junit.Test; +import sonia.scm.repository.Repository; import sonia.scm.security.UUIDKeyGenerator; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + /** * * @author Sebastian Sdorra */ -public class JAXBDataStoreTest extends DataStoreTestBase -{ +public class JAXBDataStoreTest extends DataStoreTestBase { /** * Method description @@ -52,6 +56,33 @@ public class JAXBDataStoreTest extends DataStoreTestBase @Override protected DataStoreFactory createDataStoreFactory() { - return new JAXBDataStoreFactory(contextProvider, new UUIDKeyGenerator()); + return new JAXBDataStoreFactory(contextProvider, repositoryLocationResolver, new UUIDKeyGenerator()); + } + + @Override + protected DataStore getDataStore(Class type, Repository repository) { + return createDataStoreFactory() + .withType(type) + .withName("test") + .forRepository(repository) + .build(); + } + + @Override + protected DataStore getDataStore(Class type) { + return createDataStoreFactory() + .withType(type) + .withName("test") + .build(); + } + + @Test + public void shouldStoreAndLoadInRepository() + { + repoStore.put("abc", new StoreObject("abc_value")); + StoreObject storeObject = repoStore.get("abc"); + + assertNotNull(storeObject); + assertEquals("abc_value", storeObject.getValue()); } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsBlobStoreFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsBlobStoreFactory.java index eebd6b8f2b..84733b9ea3 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsBlobStoreFactory.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsBlobStoreFactory.java @@ -74,7 +74,11 @@ public class LfsBlobStoreFactory { * * @return blob store for the corresponding scm repository */ + @SuppressWarnings("unchecked") public BlobStore getLfsBlobStore(Repository repository) { - return blobStoreFactory.getBlobStore(repository.getId() + GIT_LFS_REPOSITORY_POSTFIX); + return blobStoreFactory + .withName(repository.getId() + GIT_LFS_REPOSITORY_POSTFIX) + .forRepository(repository) + .build(); } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java index d3eca1d6e0..dbd67a7f8e 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java @@ -33,6 +33,7 @@ package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -44,6 +45,8 @@ import java.io.File; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; //~--- JDK imports ------------------------------------------------------------ @@ -81,6 +84,10 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { assertTrue(refs.isDirectory()); } + @Before + public void initFactory() { + when(factory.withType(any())).thenCallRealMethod(); + } @Override protected RepositoryHandler createRepositoryHandler(ConfigurationStoreFactory factory, diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LfsBlobStoreFactoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LfsBlobStoreFactoryTest.java index 991e2655f7..93eadf8935 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LfsBlobStoreFactoryTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LfsBlobStoreFactoryTest.java @@ -40,9 +40,12 @@ import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.Repository; import sonia.scm.store.BlobStoreFactory; -import static org.mockito.Matchers.matches; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; /** * Unit tests for {@link LfsBlobStoreFactory}. @@ -59,15 +62,21 @@ public class LfsBlobStoreFactoryTest { private LfsBlobStoreFactory lfsBlobStoreFactory; @Test - public void getBlobStore() throws Exception { - lfsBlobStoreFactory.getLfsBlobStore(new Repository("the-id", "GIT", "space", "the-name")); + public void getBlobStore() { + when(blobStoreFactory.withName(any())).thenCallRealMethod(); + Repository repository = new Repository("the-id", "GIT", "space", "the-name"); + lfsBlobStoreFactory.getLfsBlobStore(repository); // just make sure the right parameter is passed, as properly validating the return value is nearly impossible with // the return value (and should not be part of this test) - verify(blobStoreFactory).getBlobStore(matches("the-id-git-lfs")); + verify(blobStoreFactory).getStore(argThat(blobStoreParameters -> { + assertThat(blobStoreParameters.getName()).isEqualTo("the-id-git-lfs"); + assertThat(blobStoreParameters.getRepository()).isEqualTo(repository); + return true; + })); // make sure there have been no further usages of the factory - verifyNoMoreInteractions(blobStoreFactory); + verify(blobStoreFactory, times(1)).getStore(any()); } } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgRepositoryHandlerTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgRepositoryHandlerTest.java index 7a13c06eb2..c45d9ab358 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgRepositoryHandlerTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgRepositoryHandlerTest.java @@ -34,6 +34,7 @@ package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -44,6 +45,8 @@ import java.io.File; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; //~--- JDK imports ------------------------------------------------------------ @@ -67,6 +70,11 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { assertTrue(hgDirectory.isDirectory()); } + @Before + public void initFactory() { + when(factory.withType(any())).thenCallRealMethod(); + } + @Override protected RepositoryHandler createRepositoryHandler(ConfigurationStoreFactory factory, RepositoryLocationResolver locationResolver, File directory) { HgRepositoryHandler handler = new HgRepositoryHandler(factory, new HgContextProvider(), locationResolver); diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgHookCallbackServletTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgHookCallbackServletTest.java new file mode 100644 index 0000000000..7d74024630 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgHookCallbackServletTest.java @@ -0,0 +1,36 @@ +package sonia.scm.web; + +import org.junit.Test; +import sonia.scm.repository.HgRepositoryHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static sonia.scm.web.HgHookCallbackServlet.PARAM_REPOSITORYID; + +public class HgHookCallbackServletTest { + + @Test + public void shouldExtractCorrectRepositoryId() throws ServletException, IOException { + HgRepositoryHandler handler = mock(HgRepositoryHandler.class); + HgHookCallbackServlet servlet = new HgHookCallbackServlet(null, handler, null, null); + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + + when(request.getContextPath()).thenReturn("http://example.com/scm"); + when(request.getRequestURI()).thenReturn("http://example.com/scm/hook/hg/pretxnchangegroup"); + String path = "/tmp/hg/12345"; + when(request.getParameter(PARAM_REPOSITORYID)).thenReturn(path); + + servlet.doPost(request, response); + + verify(response, never()).sendError(anyInt()); + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java index 97698d7a77..86f99cd517 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java @@ -55,6 +55,7 @@ import sonia.scm.logging.SVNKitLogger; import sonia.scm.plugin.Extension; import sonia.scm.repository.spi.HookEventFacade; import sonia.scm.repository.spi.SvnRepositoryServiceProvider; +import sonia.scm.store.ConfigurationStore; import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.util.Util; diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/SvnRepositoryHandlerTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/SvnRepositoryHandlerTest.java index 7b11d1bb7f..7b22e15c94 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/SvnRepositoryHandlerTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/SvnRepositoryHandlerTest.java @@ -32,6 +32,7 @@ package sonia.scm.repository; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -42,12 +43,14 @@ import sonia.scm.store.ConfigurationStore; import sonia.scm.store.ConfigurationStoreFactory; import java.io.File; +import java.io.IOException; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; //~--- JDK imports ------------------------------------------------------------ @@ -55,15 +58,11 @@ import static org.mockito.Mockito.when; * * @author Sebastian Sdorra */ -@RunWith(MockitoJUnitRunner.class) public class SvnRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { @Mock private ConfigurationStoreFactory factory; - @Mock - private ConfigurationStore store; - @Mock private com.google.inject.Provider repositoryManagerProvider; @@ -71,6 +70,12 @@ public class SvnRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { private HookEventFacade facade = new HookEventFacade(repositoryManagerProvider, hookContextFactory); + @Override + protected void postSetUp() throws IOException, RepositoryPathNotFoundException { + initMocks(this); + super.postSetUp(); + } + @Override protected void checkDirectory(File directory) { File format = new File(directory, "format"); @@ -102,7 +107,7 @@ public class SvnRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { @Test public void getDirectory() { - when(factory.getStore(any(), any())).thenReturn(store); + when(factory.withType(any())).thenCallRealMethod(); SvnRepositoryHandler repositoryHandler = new SvnRepositoryHandler(factory, facade, locationResolver); diff --git a/scm-test/src/main/java/sonia/scm/AbstractTestBase.java b/scm-test/src/main/java/sonia/scm/AbstractTestBase.java index 13cde0391e..040b347e4a 100644 --- a/scm-test/src/main/java/sonia/scm/AbstractTestBase.java +++ b/scm-test/src/main/java/sonia/scm/AbstractTestBase.java @@ -46,10 +46,15 @@ import org.junit.After; import org.junit.AfterClass; import org.junit.Before; +import sonia.scm.io.DefaultFileSystem; +import sonia.scm.repository.InitialRepositoryLocationResolver; +import sonia.scm.repository.RepositoryDAO; +import sonia.scm.repository.RepositoryLocationResolver; import sonia.scm.util.IOUtil; import sonia.scm.util.MockUtil; import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; //~--- JDK imports ------------------------------------------------------------ @@ -66,10 +71,29 @@ import java.util.logging.Logger; public class AbstractTestBase { - /** Field description */ private static ThreadState subjectThreadState; - //~--- methods -------------------------------------------------------------- + protected SCMContextProvider contextProvider; + + private File tempDirectory; + + protected DefaultFileSystem fileSystem; + + protected RepositoryDAO repositoryDAO = mock(RepositoryDAO.class); + protected RepositoryLocationResolver repositoryLocationResolver; + + @Before + public void setUpTest() throws Exception + { + tempDirectory = new File(System.getProperty("java.io.tmpdir"), + UUID.randomUUID().toString()); + assertTrue(tempDirectory.mkdirs()); + contextProvider = MockUtil.getSCMContextProvider(tempDirectory); + fileSystem = new DefaultFileSystem(); + InitialRepositoryLocationResolver initialRepoLocationResolver = new InitialRepositoryLocationResolver(); + repositoryLocationResolver = new RepositoryLocationResolver(contextProvider, repositoryDAO, initialRepoLocationResolver); + postSetUp(); + } /** * Method description @@ -165,25 +189,6 @@ public class AbstractTestBase } } - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @throws Exception - */ - @Before - public void setUpTest() throws Exception - { - tempDirectory = new File(System.getProperty("java.io.tmpdir"), - UUID.randomUUID().toString()); - assertTrue(tempDirectory.mkdirs()); - contextProvider = MockUtil.getSCMContextProvider(tempDirectory); - postSetUp(); - } - - //~--- methods -------------------------------------------------------------- /** * Clears Shiro's thread state, ensuring the thread remains clean for @@ -249,12 +254,4 @@ public class AbstractTestBase subjectThreadState = createThreadState(subject); subjectThreadState.bind(); } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - protected SCMContextProvider contextProvider; - - /** Field description */ - private File tempDirectory; } diff --git a/scm-test/src/main/java/sonia/scm/ManagerTestBase.java b/scm-test/src/main/java/sonia/scm/ManagerTestBase.java index a12fb39726..823e88c9fc 100644 --- a/scm-test/src/main/java/sonia/scm/ManagerTestBase.java +++ b/scm-test/src/main/java/sonia/scm/ManagerTestBase.java @@ -37,11 +37,16 @@ import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.rules.TemporaryFolder; +import sonia.scm.repository.InitialRepositoryLocationResolver; +import sonia.scm.repository.RepositoryDAO; +import sonia.scm.repository.RepositoryLocationResolver; import sonia.scm.util.MockUtil; import java.io.File; import java.io.IOException; +import static org.mockito.Mockito.mock; + /** * * @author Sebastian Sdorra @@ -55,14 +60,21 @@ public abstract class ManagerTestBase public TemporaryFolder tempFolder = new TemporaryFolder(); protected SCMContextProvider contextProvider; - + protected RepositoryLocationResolver locationResolver; + protected Manager manager; - protected File temp; + + protected File temp ; @Before public void setUp() throws IOException { - temp = tempFolder.newFolder(); + if (temp == null){ + temp = tempFolder.newFolder(); + } contextProvider = MockUtil.getSCMContextProvider(temp); + InitialRepositoryLocationResolver initialRepositoryLocationResolver = new InitialRepositoryLocationResolver(); + RepositoryDAO repoDao = mock(RepositoryDAO.class); + locationResolver = new RepositoryLocationResolver(contextProvider, repoDao ,initialRepositoryLocationResolver); manager = createManager(); manager.init(contextProvider); } diff --git a/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java b/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java index 37f7266984..f48744d460 100644 --- a/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java +++ b/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java @@ -43,7 +43,6 @@ import java.io.File; import java.io.IOException; import java.nio.file.Path; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; @@ -99,7 +98,6 @@ public abstract class SimpleRepositoryHandlerTestBase extends AbstractTestBase { handler.create(repository); - assertTrue(nativeRepoDirectory.exists()); assertTrue(nativeRepoDirectory.isDirectory()); checkDirectory(nativeRepoDirectory); diff --git a/scm-test/src/main/java/sonia/scm/store/BlobStoreTestBase.java b/scm-test/src/main/java/sonia/scm/store/BlobStoreTestBase.java index 48504feaf2..f3f252053d 100644 --- a/scm-test/src/main/java/sonia/scm/store/BlobStoreTestBase.java +++ b/scm-test/src/main/java/sonia/scm/store/BlobStoreTestBase.java @@ -35,22 +35,24 @@ package sonia.scm.store; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.io.ByteStreams; - import org.junit.Before; import org.junit.Test; - import sonia.scm.AbstractTestBase; - -import static org.junit.Assert.*; - -//~--- JDK imports ------------------------------------------------------------ +import sonia.scm.repository.RepositoryTestData; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; - import java.util.List; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -58,12 +60,6 @@ import java.util.List; public abstract class BlobStoreTestBase extends AbstractTestBase { - /** - * Method description - * - * - * @return - */ protected abstract BlobStoreFactory createBlobStoreFactory(); /** @@ -73,7 +69,10 @@ public abstract class BlobStoreTestBase extends AbstractTestBase @Before public void createBlobStore() { - store = createBlobStoreFactory().getBlobStore("test"); + store = createBlobStoreFactory() + .withName("test") + .forRepository(RepositoryTestData.createHeartOfGold()) + .build(); store.clear(); } diff --git a/scm-test/src/main/java/sonia/scm/store/ConfigurationEntryStoreTestBase.java b/scm-test/src/main/java/sonia/scm/store/ConfigurationEntryStoreTestBase.java index 8d3a63717a..140bd54e65 100644 --- a/scm-test/src/main/java/sonia/scm/store/ConfigurationEntryStoreTestBase.java +++ b/scm-test/src/main/java/sonia/scm/store/ConfigurationEntryStoreTestBase.java @@ -32,12 +32,13 @@ package sonia.scm.store; +import sonia.scm.repository.Repository; + /** * * @author Sebastian Sdorra */ -public abstract class ConfigurationEntryStoreTestBase extends KeyValueStoreTestBase -{ +public abstract class ConfigurationEntryStoreTestBase extends KeyValueStoreTestBase { /** * Method description @@ -48,17 +49,20 @@ public abstract class ConfigurationEntryStoreTestBase extends KeyValueStoreTestB protected abstract ConfigurationEntryStoreFactory createConfigurationStoreFactory(); //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ @Override - protected ConfigurationEntryStore getDataStore() - { - return createConfigurationStoreFactory().getStore(StoreObject.class, - "test"); + protected ConfigurationEntryStore getDataStore(Class type) { + return this.createConfigurationStoreFactory() + .withType(type) + .withName(storeName) + .build(); + } + + @Override + protected ConfigurationEntryStore getDataStore(Class type, Repository repository) { + return this.createConfigurationStoreFactory() + .withType(type) + .withName(repoStoreName) + .forRepository(repository) + .build(); } } diff --git a/scm-test/src/main/java/sonia/scm/store/DataStoreTestBase.java b/scm-test/src/main/java/sonia/scm/store/DataStoreTestBase.java index 3129d3a339..39ce021715 100644 --- a/scm-test/src/main/java/sonia/scm/store/DataStoreTestBase.java +++ b/scm-test/src/main/java/sonia/scm/store/DataStoreTestBase.java @@ -33,6 +33,13 @@ package sonia.scm.store; +import org.junit.Test; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryTestData; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + /** * * @author Sebastian Sdorra @@ -48,17 +55,29 @@ public abstract class DataStoreTestBase extends KeyValueStoreTestBase */ protected abstract DataStoreFactory createDataStoreFactory(); + //~--- get methods ---------------------------------------------------------- - /** - * Method description - * - * - * @return - */ - @Override - protected DataStore getDataStore() + + + + @Test + public void shouldStoreRepositorySpecificData() { - return createDataStoreFactory().getStore(StoreObject.class, "test"); + DataStoreFactory dataStoreFactory = createDataStoreFactory(); + StoreObject obj = new StoreObject("test-1"); + Repository repository = RepositoryTestData.createHeartOfGold(); + + DataStore store = dataStoreFactory + .withType(StoreObject.class) + .withName("test") + .forRepository(repository) + .build(); + + String id = store.put(obj); + + assertNotNull(id); + + assertEquals(obj, store.get(id)); } } diff --git a/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationStoreFactory.java b/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationStoreFactory.java index d5e9474ff5..2c5641bfd1 100644 --- a/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationStoreFactory.java +++ b/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationStoreFactory.java @@ -43,8 +43,7 @@ package sonia.scm.store; public class InMemoryConfigurationStoreFactory implements ConfigurationStoreFactory { @Override - public ConfigurationStore getStore(Class type, String name) - { + public ConfigurationStore getStore(TypedStoreParameters storeParameters) { return new InMemoryConfigurationStore<>(); } } diff --git a/scm-test/src/main/java/sonia/scm/store/KeyValueStoreTestBase.java b/scm-test/src/main/java/sonia/scm/store/KeyValueStoreTestBase.java index 0abad4f558..a54b58178f 100644 --- a/scm-test/src/main/java/sonia/scm/store/KeyValueStoreTestBase.java +++ b/scm-test/src/main/java/sonia/scm/store/KeyValueStoreTestBase.java @@ -38,6 +38,8 @@ import org.junit.Before; import org.junit.Test; import sonia.scm.AbstractTestBase; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryTestData; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -56,13 +58,21 @@ import java.util.Map; public abstract class KeyValueStoreTestBase extends AbstractTestBase { + protected Repository repository = RepositoryTestData.createHeartOfGold(); + protected DataStore store; + protected DataStore repoStore; + protected String repoStoreName = "testRepoStore"; + protected String storeName = "testStore"; + /** * Method description * * * @return */ - protected abstract DataStore getDataStore(); + protected abstract DataStore getDataStore(Class type , Repository repository); + protected abstract DataStore getDataStore(Class type ); + //~--- methods -------------------------------------------------------------- @@ -73,8 +83,10 @@ public abstract class KeyValueStoreTestBase extends AbstractTestBase @Before public void before() { - store = getDataStore(); + store = getDataStore(StoreObject.class); + repoStore = getDataStore(StoreObject.class, repository); store.clear(); + repoStore.clear(); } /** @@ -215,8 +227,5 @@ public abstract class KeyValueStoreTestBase extends AbstractTestBase assertNull(store.get("2")); } - //~--- fields --------------------------------------------------------------- - /** Field description */ - private DataStore store; } diff --git a/scm-test/src/main/java/sonia/scm/store/StoreTestBase.java b/scm-test/src/main/java/sonia/scm/store/StoreTestBase.java index c39efa3ffe..ef806c79f8 100644 --- a/scm-test/src/main/java/sonia/scm/store/StoreTestBase.java +++ b/scm-test/src/main/java/sonia/scm/store/StoreTestBase.java @@ -35,10 +35,11 @@ package sonia.scm.store; //~--- non-JDK imports -------------------------------------------------------- import org.junit.Test; - import sonia.scm.AbstractTestBase; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; //~--- JDK imports ------------------------------------------------------------ @@ -65,8 +66,7 @@ public abstract class StoreTestBase extends AbstractTestBase @Test public void testGet() { - ConfigurationStore store = createStoreFactory().getStore(StoreObject.class, - "test"); + ConfigurationStore store = createStoreFactory().withType(StoreObject.class).withName("test").build(); assertNotNull(store); @@ -82,8 +82,7 @@ public abstract class StoreTestBase extends AbstractTestBase @Test public void testSet() { - ConfigurationStore store = createStoreFactory().getStore(StoreObject.class, - "test"); + ConfigurationStore store = createStoreFactory().withType(StoreObject.class).withName("test").build(); assertNotNull(store); diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index 4dfb749690..01f45cb54e 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -72,8 +72,18 @@ io.jsonwebtoken - jjwt - 0.4 + jjwt-impl + ${jjwt.version} + + + io.jsonwebtoken + jjwt-api + ${jjwt.version} + + + io.jsonwebtoken + jjwt-jackson + ${jjwt.version} @@ -540,6 +550,7 @@ DEVELOPMENT target/scm-it default + 0.10.5 2.53.1 1.0 0.8.17 diff --git a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java index 90764a7e00..9555ad66b5 100644 --- a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java +++ b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java @@ -83,8 +83,10 @@ import sonia.scm.security.AuthorizationChangedEventProducer; import sonia.scm.security.CipherHandler; import sonia.scm.security.CipherUtil; import sonia.scm.security.ConfigurableLoginAttemptHandler; +import sonia.scm.security.DefaultJwtAccessTokenRefreshStrategy; import sonia.scm.security.DefaultKeyGenerator; import sonia.scm.security.DefaultSecuritySystem; +import sonia.scm.security.JwtAccessTokenRefreshStrategy; import sonia.scm.security.KeyGenerator; import sonia.scm.security.LoginAttemptHandler; import sonia.scm.security.SecuritySystem; diff --git a/scm-webapp/src/main/java/sonia/scm/debug/DebugService.java b/scm-webapp/src/main/java/sonia/scm/debug/DebugService.java index 8e2475d802..5fb5925e6c 100644 --- a/scm-webapp/src/main/java/sonia/scm/debug/DebugService.java +++ b/scm-webapp/src/main/java/sonia/scm/debug/DebugService.java @@ -52,7 +52,7 @@ public final class DebugService private final Multimap receivedHooks = LinkedListMultimap.create(); /** - * Stores {@link DebugHookData} for the given repository. + * Store {@link DebugHookData} for the given repository. */ void put(NamespaceAndName namespaceAndName, DebugHookData hookData) { diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultJwtAccessTokenRefreshStrategy.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultJwtAccessTokenRefreshStrategy.java new file mode 100644 index 0000000000..266a327d44 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultJwtAccessTokenRefreshStrategy.java @@ -0,0 +1,10 @@ +package sonia.scm.security; + +import sonia.scm.plugin.Extension; + +@Extension +public class DefaultJwtAccessTokenRefreshStrategy extends PercentageJwtAccessTokenRefreshStrategy { + public DefaultJwtAccessTokenRefreshStrategy() { + super(0.5F); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java index d958dcf41f..e93d4de597 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java @@ -108,9 +108,13 @@ public class DefaultSecuritySystem implements SecuritySystem * @param storeFactory */ @Inject + @SuppressWarnings("unchecked") public DefaultSecuritySystem(ConfigurationEntryStoreFactory storeFactory) { - store = storeFactory.getStore(AssignedPermission.class, NAME); + store = storeFactory + .withType(AssignedPermission.class) + .withName(NAME) + .build(); readAvailablePermissions(); } diff --git a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessToken.java b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessToken.java index 46f4c68e74..8fb5929188 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessToken.java +++ b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessToken.java @@ -31,9 +31,14 @@ package sonia.scm.security; import io.jsonwebtoken.Claims; + +import java.util.Collections; import java.util.Date; +import java.util.Map; import java.util.Optional; +import static java.util.Optional.ofNullable; + /** * Jwt implementation of {@link AccessToken}. * @@ -41,7 +46,9 @@ import java.util.Optional; * @since 2.0.0 */ public final class JwtAccessToken implements AccessToken { - + + public static final String REFRESHABLE_UNTIL_CLAIM_KEY = "scm-manager.refreshExpiration"; + public static final String PARENT_TOKEN_ID_CLAIM_KEY = "scm-manager.parentTokenId"; private final Claims claims; private final String compact; @@ -75,6 +82,15 @@ public final class JwtAccessToken implements AccessToken { return claims.getExpiration(); } + @Override + public Optional getRefreshExpiration() { + return ofNullable(claims.get(REFRESHABLE_UNTIL_CLAIM_KEY, Date.class)); + } + + public Optional getParentKey() { + return ofNullable(claims.get(PARENT_TOKEN_ID_CLAIM_KEY).toString()); + } + @Override public Scope getScope() { return Scopes.fromClaims(claims); @@ -90,5 +106,9 @@ public final class JwtAccessToken implements AccessToken { public String compact() { return compact; } - + + @Override + public Map getClaims() { + return Collections.unmodifiableMap(claims); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java index ece96e2954..66db720125 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java +++ b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java @@ -36,10 +36,12 @@ import com.google.common.collect.Maps; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; + +import java.time.Clock; +import java.time.Instant; import java.util.Date; import java.util.HashMap; import java.util.Map; -import java.util.Set; import java.util.concurrent.TimeUnit; import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.Subject; @@ -48,7 +50,7 @@ import org.slf4j.LoggerFactory; /** * Jwt implementation of {@link AccessTokenBuilder}. - * + * * @author Sebastian Sdorra * @since 2.0.0 */ @@ -58,21 +60,27 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder { * the logger for JwtAccessTokenBuilder */ private static final Logger LOG = LoggerFactory.getLogger(JwtAccessTokenBuilder.class); - - private final KeyGenerator keyGenerator; - private final SecureKeyResolver keyResolver; - + + private final KeyGenerator keyGenerator; + private final SecureKeyResolver keyResolver; + private final Clock clock; + private String subject; private String issuer; - private long expiresIn = 60l; - private TimeUnit expiresInUnit = TimeUnit.MINUTES; + private long expiresIn = 1; + private TimeUnit expiresInUnit = TimeUnit.HOURS; + private long refreshableFor = 12; + private TimeUnit refreshableForUnit = TimeUnit.HOURS; + private Instant refreshExpiration; + private String parentKeyId; private Scope scope = Scope.empty(); - + private final Map custom = Maps.newHashMap(); - - JwtAccessTokenBuilder(KeyGenerator keyGenerator, SecureKeyResolver keyResolver) { + + JwtAccessTokenBuilder(KeyGenerator keyGenerator, SecureKeyResolver keyResolver, Clock clock) { this.keyGenerator = keyGenerator; this.keyResolver = keyResolver; + this.clock = clock; } @Override @@ -81,7 +89,7 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder { this.subject = subject; return this; } - + @Override public JwtAccessTokenBuilder custom(String key, Object value) { Preconditions.checkArgument(!Strings.isNullOrEmpty(key), "null or empty value not allowed"); @@ -92,11 +100,11 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder { @Override public JwtAccessTokenBuilder scope(Scope scope) { - Preconditions.checkArgument(scope != null, "scope can not be null"); + Preconditions.checkArgument(scope != null, "scope cannot be null"); this.scope = scope; return this; } - + @Override public JwtAccessTokenBuilder issuer(String issuer) { Preconditions.checkArgument(!Strings.isNullOrEmpty(issuer), "null or empty value not allowed"); @@ -106,15 +114,37 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder { @Override public JwtAccessTokenBuilder expiresIn(long count, TimeUnit unit) { - Preconditions.checkArgument(count > 0, "expires in must be greater than 0"); - Preconditions.checkArgument(unit != null, "unit can not be null"); - + Preconditions.checkArgument(count > 0, "count must be greater than 0"); + Preconditions.checkArgument(unit != null, "unit cannot be null"); + this.expiresIn = count; this.expiresInUnit = unit; - + return this; } - + + @Override + public JwtAccessTokenBuilder refreshableFor(long count, TimeUnit unit) { + Preconditions.checkArgument(count >= 0, "count must be greater or equal to 0"); + Preconditions.checkArgument(unit != null, "unit cannot be null"); + + this.refreshableFor = count; + this.refreshableForUnit = unit; + + return this; + } + + JwtAccessTokenBuilder refreshExpiration(Instant refreshExpiration) { + this.refreshExpiration = refreshExpiration; + this.refreshableFor = 0; + return this; + } + + public JwtAccessTokenBuilder parentKey(String parentKeyId) { + this.parentKeyId = parentKeyId; + return this; + } + private String getSubject(){ if (subject == null) { Subject currentSubject = SecurityUtils.getSubject(); @@ -130,35 +160,48 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder { String id = keyGenerator.createKey(); String sub = getSubject(); - + LOG.trace("create new token {} for user {}", id, subject); SecureKey key = keyResolver.getSecureKey(sub); - + Map customClaims = new HashMap<>(custom); - + // add scope to custom claims Scopes.toClaims(customClaims, scope); - - Date now = new Date(); + + Instant now = clock.instant(); long expiration = expiresInUnit.toMillis(expiresIn); - + Claims claims = Jwts.claims(customClaims) .setSubject(sub) .setId(id) - .setIssuedAt(now) - .setExpiration(new Date(now.getTime() + expiration)); - + .setIssuedAt(Date.from(now)) + .setExpiration(new Date(now.toEpochMilli() + expiration)); + + + if (refreshableFor > 0) { + long refreshExpiration = refreshableForUnit.toMillis(refreshableFor); + claims.put(JwtAccessToken.REFRESHABLE_UNTIL_CLAIM_KEY, new Date(now.toEpochMilli() + refreshExpiration).getTime()); + } else if (refreshExpiration != null) { + claims.put(JwtAccessToken.REFRESHABLE_UNTIL_CLAIM_KEY, Date.from(refreshExpiration)); + } + if (parentKeyId == null) { + claims.put(JwtAccessToken.PARENT_TOKEN_ID_CLAIM_KEY, id); + } else { + claims.put(JwtAccessToken.PARENT_TOKEN_ID_CLAIM_KEY, parentKeyId); + } + if ( issuer != null ) { claims.setIssuer(issuer); } - + // sign token and create compact version String compact = Jwts.builder() .setClaims(claims) .signWith(SignatureAlgorithm.HS256, key.getBytes()) .compact(); - + return new JwtAccessToken(claims, compact); } - + } diff --git a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilderFactory.java b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilderFactory.java index 63c4ea981c..a5704b0e82 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilderFactory.java +++ b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilderFactory.java @@ -30,6 +30,7 @@ */ package sonia.scm.security; +import java.time.Clock; import java.util.Set; import javax.inject.Inject; import sonia.scm.plugin.Extension; @@ -46,19 +47,25 @@ public final class JwtAccessTokenBuilderFactory implements AccessTokenBuilderFac private final KeyGenerator keyGenerator; private final SecureKeyResolver keyResolver; private final Set enrichers; + private final Clock clock; @Inject public JwtAccessTokenBuilderFactory( - KeyGenerator keyGenerator, SecureKeyResolver keyResolver, Set enrichers - ) { + KeyGenerator keyGenerator, SecureKeyResolver keyResolver, Set enrichers) { + this(keyGenerator, keyResolver, enrichers, Clock.systemDefaultZone()); + } + + JwtAccessTokenBuilderFactory( + KeyGenerator keyGenerator, SecureKeyResolver keyResolver, Set enrichers, Clock clock) { this.keyGenerator = keyGenerator; this.keyResolver = keyResolver; this.enrichers = enrichers; + this.clock = clock; } @Override public JwtAccessTokenBuilder create() { - JwtAccessTokenBuilder builder = new JwtAccessTokenBuilder(keyGenerator, keyResolver); + JwtAccessTokenBuilder builder = new JwtAccessTokenBuilder(keyGenerator, keyResolver, clock); // enrich access token builder enrichers.forEach((enricher) -> { diff --git a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenRefreshStrategy.java b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenRefreshStrategy.java new file mode 100644 index 0000000000..9135a0e099 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenRefreshStrategy.java @@ -0,0 +1,8 @@ +package sonia.scm.security; + +import sonia.scm.plugin.ExtensionPoint; + +@ExtensionPoint(multi = false) +public interface JwtAccessTokenRefreshStrategy { + boolean shouldBeRefreshed(JwtAccessToken oldToken); +} diff --git a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenRefresher.java b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenRefresher.java new file mode 100644 index 0000000000..6db01c904f --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenRefresher.java @@ -0,0 +1,77 @@ +package sonia.scm.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import java.time.Clock; +import java.util.Date; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +public class JwtAccessTokenRefresher { + + private static final Logger log = LoggerFactory.getLogger(JwtAccessTokenRefresher.class); + + private final JwtAccessTokenBuilderFactory builderFactory; + private final JwtAccessTokenRefreshStrategy refreshStrategy; + private final Clock clock; + + @Inject + public JwtAccessTokenRefresher(JwtAccessTokenBuilderFactory builderFactory, JwtAccessTokenRefreshStrategy refreshStrategy) { + this(builderFactory, refreshStrategy, Clock.systemDefaultZone()); + } + + JwtAccessTokenRefresher(JwtAccessTokenBuilderFactory builderFactory, JwtAccessTokenRefreshStrategy refreshStrategy, Clock clock) { + this.builderFactory = builderFactory; + this.refreshStrategy = refreshStrategy; + this.clock = clock; + } + + @SuppressWarnings("squid:S3655") // the refresh expiration cannot be null at the time building the new token, because + // we checked this before in tokenCanBeRefreshed + public Optional refresh(JwtAccessToken oldToken) { + JwtAccessTokenBuilder builder = builderFactory.create(); + Map claims = oldToken.getClaims(); + claims.forEach(builder::custom); + + if (canBeRefreshed(oldToken) && shouldBeRefreshed(oldToken)) { + Optional parentTokenId = oldToken.getCustom("scm-manager.parentTokenId"); + if (!parentTokenId.isPresent()) { + log.warn("no parent token id found in token; could not refresh"); + return Optional.empty(); + } + builder.expiresIn(computeOldExpirationInMillis(oldToken), TimeUnit.MILLISECONDS); + builder.parentKey(parentTokenId.get().toString()); + builder.refreshExpiration(oldToken.getRefreshExpiration().get().toInstant()); + return Optional.of(builder.build()); + } else { + return Optional.empty(); + } + } + + private long computeOldExpirationInMillis(JwtAccessToken oldToken) { + return oldToken.getExpiration().getTime() - oldToken.getIssuedAt().getTime(); + } + + private boolean canBeRefreshed(JwtAccessToken oldToken) { + return tokenIsValid(oldToken) && tokenCanBeRefreshed(oldToken); + } + + private boolean shouldBeRefreshed(JwtAccessToken oldToken) { + return refreshStrategy.shouldBeRefreshed(oldToken); + } + + private boolean tokenCanBeRefreshed(JwtAccessToken oldToken) { + return oldToken.getRefreshExpiration().map(this::isAfterNow).orElse(false); + } + + private boolean tokenIsValid(JwtAccessToken oldToken) { + return isAfterNow(oldToken.getExpiration()); + } + + private boolean isAfterNow(Date expiration) { + return expiration.toInstant().isAfter(clock.instant()); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/security/PercentageJwtAccessTokenRefreshStrategy.java b/scm-webapp/src/main/java/sonia/scm/security/PercentageJwtAccessTokenRefreshStrategy.java new file mode 100644 index 0000000000..c78654c389 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/security/PercentageJwtAccessTokenRefreshStrategy.java @@ -0,0 +1,25 @@ +package sonia.scm.security; + +import java.time.Clock; + +public class PercentageJwtAccessTokenRefreshStrategy implements JwtAccessTokenRefreshStrategy { + + private final Clock clock; + private final float refreshPercentage; + + public PercentageJwtAccessTokenRefreshStrategy(float refreshPercentage) { + this(Clock.systemDefaultZone(), refreshPercentage); + } + + PercentageJwtAccessTokenRefreshStrategy(Clock clock, float refreshPercentage) { + this.clock = clock; + this.refreshPercentage = refreshPercentage; + } + + @Override + public boolean shouldBeRefreshed(JwtAccessToken oldToken) { + long liveSpan = oldToken.getExpiration().getTime() - oldToken.getIssuedAt().getTime(); + long age = clock.instant().toEpochMilli() - oldToken.getIssuedAt().getTime(); + return (float)age/liveSpan > refreshPercentage; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/security/SecureKeyResolver.java b/scm-webapp/src/main/java/sonia/scm/security/SecureKeyResolver.java index c7c594d5e3..a369db66bd 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/SecureKeyResolver.java +++ b/scm-webapp/src/main/java/sonia/scm/security/SecureKeyResolver.java @@ -87,9 +87,13 @@ public class SecureKeyResolver extends SigningKeyResolverAdapter * @param storeFactory store factory */ @Inject + @SuppressWarnings("unchecked") public SecureKeyResolver(ConfigurationEntryStoreFactory storeFactory) { - this.store = storeFactory.getStore(SecureKey.class, STORE_NAME); + store = storeFactory + .withType(SecureKey.class) + .withName(STORE_NAME) + .build(); } //~--- methods -------------------------------------------------------------- diff --git a/scm-webapp/src/main/java/sonia/scm/web/security/TokenRefreshFilter.java b/scm-webapp/src/main/java/sonia/scm/web/security/TokenRefreshFilter.java new file mode 100644 index 0000000000..f85c0fbbbd --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/web/security/TokenRefreshFilter.java @@ -0,0 +1,78 @@ +package sonia.scm.web.security; + +import org.apache.shiro.authc.AuthenticationToken; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.Priority; +import sonia.scm.filter.Filters; +import sonia.scm.filter.WebElement; +import sonia.scm.security.AccessToken; +import sonia.scm.security.AccessTokenCookieIssuer; +import sonia.scm.security.AccessTokenResolver; +import sonia.scm.security.BearerToken; +import sonia.scm.security.JwtAccessToken; +import sonia.scm.security.JwtAccessTokenRefresher; +import sonia.scm.web.WebTokenGenerator; +import sonia.scm.web.filter.HttpFilter; + +import javax.inject.Inject; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Optional; +import java.util.Set; + +import static java.util.Optional.empty; +import static java.util.Optional.of; + +@Priority(Filters.PRIORITY_POST_AUTHENTICATION) +@WebElement(value = Filters.PATTERN_RESTAPI, + morePatterns = { Filters.PATTERN_DEBUG }) +public class TokenRefreshFilter extends HttpFilter { + + private static final Logger LOG = LoggerFactory.getLogger(TokenRefreshFilter.class); + + private final Set tokenGenerators; + private final JwtAccessTokenRefresher refresher; + private final AccessTokenResolver resolver; + private final AccessTokenCookieIssuer issuer; + + @Inject + public TokenRefreshFilter(Set tokenGenerators, JwtAccessTokenRefresher refresher, AccessTokenResolver resolver, AccessTokenCookieIssuer issuer) { + this.tokenGenerators = tokenGenerators; + this.refresher = refresher; + this.resolver = resolver; + this.issuer = issuer; + } + + @Override + protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { + extractToken(request).ifPresent(token -> examineToken(request, response, token)); + chain.doFilter(request, response); + } + + private Optional extractToken(HttpServletRequest request) { + for (WebTokenGenerator generator : tokenGenerators) { + AuthenticationToken token = generator.createToken(request); + if (token instanceof BearerToken) { + return of((BearerToken) token); + } + } + return empty(); + } + + private void examineToken(HttpServletRequest request, HttpServletResponse response, BearerToken token) { + AccessToken accessToken = resolver.resolve(token); + if (accessToken instanceof JwtAccessToken) { + refresher.refresh((JwtAccessToken) accessToken) + .ifPresent(jwtAccessToken -> refreshToken(request, response, jwtAccessToken)); + } + } + + private void refreshToken(HttpServletRequest request, HttpServletResponse response, JwtAccessToken jwtAccessToken) { + LOG.debug("refreshing authentication token"); + issuer.authenticate(request, response, jwtAccessToken); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AutoCompleteResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AutoCompleteResourceTest.java index 2cab41bfbf..d392aefe4e 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AutoCompleteResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AutoCompleteResourceTest.java @@ -66,7 +66,8 @@ public class AutoCompleteResourceTest { ConfigurationStore storeConfig = mock(ConfigurationStore.class); xmlDB = mock(XmlDatabase.class); when(storeConfig.get()).thenReturn(xmlDB); - when(storeFactory.getStore(any(), any())).thenReturn(storeConfig); + when(storeFactory.getStore(any())).thenReturn(storeConfig); + when(storeFactory.withType(any())).thenCallRealMethod(); XmlUserDAO userDao = new XmlUserDAO(storeFactory); this.userDao = spy(userDao); XmlGroupDAO groupDAO = new XmlGroupDAO(storeFactory); diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java index 6b81f4b328..416babfab0 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java @@ -418,10 +418,10 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase { private DefaultRepositoryManager createRepositoryManager(boolean archiveEnabled, KeyGenerator keyGenerator) { DefaultFileSystem fileSystem = new DefaultFileSystem(); Set handlerSet = new HashSet<>(); - ConfigurationStoreFactory factory = new JAXBConfigurationStoreFactory(contextProvider); InitialRepositoryLocationResolver initialRepositoryLocationResolver = new InitialRepositoryLocationResolver(); XmlRepositoryDAO repositoryDAO = new XmlRepositoryDAO(contextProvider, initialRepositoryLocationResolver, fileSystem); RepositoryLocationResolver repositoryLocationResolver = new RepositoryLocationResolver(contextProvider, repositoryDAO, initialRepositoryLocationResolver); + ConfigurationStoreFactory factory = new JAXBConfigurationStoreFactory(contextProvider, repositoryLocationResolver); handlerSet.add(new DummyRepositoryHandler(factory, repositoryLocationResolver)); handlerSet.add(new DummyRepositoryHandler(factory, repositoryLocationResolver) { @Override diff --git a/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java b/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java index e6061e61a1..26dfcb2099 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java @@ -61,7 +61,6 @@ import sonia.scm.user.UserDAO; import sonia.scm.user.UserTestData; import javax.crypto.spec.SecretKeySpec; -import java.security.SecureRandom; import java.util.Date; import java.util.Set; @@ -71,6 +70,7 @@ import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.any; import static org.mockito.Mockito.when; +import static sonia.scm.security.SecureKeyTestUtil.createSecureKey; /** * Unit tests for {@link BearerRealm}. @@ -256,12 +256,6 @@ private String createCompactToken(String subject, SecureKey key) { .compact(); } - private SecureKey createSecureKey() { - byte[] bytes = new byte[32]; - random.nextBytes(bytes); - return new SecureKey(bytes, System.currentTimeMillis()); - } - private void resolveKey(SecureKey key) { when( keyResolver.resolveSigningKey( @@ -272,16 +266,13 @@ private String createCompactToken(String subject, SecureKey key) { .thenReturn( new SecretKeySpec( key.getBytes(), - SignatureAlgorithm.HS256.getValue() + SignatureAlgorithm.HS256.getJcaName() ) ); } //~--- fields --------------------------------------------------------------- - /** Field description */ - private final SecureRandom random = new SecureRandom(); - @InjectMocks private DAORealmHelperFactory helperFactory; diff --git a/scm-webapp/src/test/java/sonia/scm/security/DefaultSecuritySystemTest.java b/scm-webapp/src/test/java/sonia/scm/security/DefaultSecuritySystemTest.java index 95ad64b349..2ccdb2b28a 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/DefaultSecuritySystemTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/DefaultSecuritySystemTest.java @@ -70,8 +70,7 @@ public class DefaultSecuritySystemTest extends AbstractTestBase public void createSecuritySystem() { JAXBConfigurationEntryStoreFactory factory = - new JAXBConfigurationEntryStoreFactory(new UUIDKeyGenerator(), - contextProvider); + new JAXBConfigurationEntryStoreFactory(contextProvider , repositoryLocationResolver, new UUIDKeyGenerator() ); securitySystem = new DefaultSecuritySystem(factory); diff --git a/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenBuilderTest.java b/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenBuilderTest.java index 6dda005019..c005e7d381 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenBuilderTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenBuilderTest.java @@ -44,7 +44,6 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import java.util.Random; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -56,6 +55,7 @@ import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.when; +import static sonia.scm.security.SecureKeyTestUtil.createSecureKey; /** * Unit test for {@link JwtAccessTokenBuilder}. @@ -162,11 +162,4 @@ public class JwtAccessTokenBuilderTest { assertEquals("b", token.getCustom("a").get()); assertEquals("[\"repo:*\"]", token.getScope().toString()); } - - private SecureKey createSecureKey() { - byte[] bytes = new byte[32]; - new Random().nextBytes(bytes); - return new SecureKey(bytes, System.currentTimeMillis()); - } - } diff --git a/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenRefresherTest.java b/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenRefresherTest.java new file mode 100644 index 0000000000..774677cde3 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenRefresherTest.java @@ -0,0 +1,152 @@ +package sonia.scm.security; + +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.sql.Date; +import java.time.Clock; +import java.time.Instant; +import java.util.Collections; +import java.util.Optional; + +import static java.time.Duration.ofMinutes; +import static java.time.temporal.ChronoUnit.SECONDS; +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static sonia.scm.security.SecureKeyTestUtil.createSecureKey; + +@SubjectAware( + username = "user", + password = "secret", + configuration = "classpath:sonia/scm/repository/shiro.ini" +) +@RunWith(MockitoJUnitRunner.class) +public class JwtAccessTokenRefresherTest { + + private static final Instant NOW = Instant.now().truncatedTo(SECONDS); + private static final Instant TOKEN_CREATION = NOW.minus(ofMinutes(1)); + + @Rule + public ShiroRule shiro = new ShiroRule(); + + @Mock + private SecureKeyResolver keyResolver; + @Mock + private JwtAccessTokenRefreshStrategy refreshStrategy; + @Mock + private Clock refreshClock; + + private KeyGenerator keyGenerator = () -> "key"; + + private JwtAccessTokenRefresher refresher; + private JwtAccessTokenBuilder tokenBuilder; + + @Before + public void initKeyResolver() { + when(keyResolver.getSecureKey(any())).thenReturn(createSecureKey()); + + Clock creationClock = mock(Clock.class); + when(creationClock.instant()).thenReturn(TOKEN_CREATION); + tokenBuilder = new JwtAccessTokenBuilderFactory(keyGenerator, keyResolver, Collections.emptySet(), creationClock).create(); + + JwtAccessTokenBuilderFactory refreshBuilderFactory = new JwtAccessTokenBuilderFactory(keyGenerator, keyResolver, Collections.emptySet(), refreshClock); + refresher = new JwtAccessTokenRefresher(refreshBuilderFactory, refreshStrategy, refreshClock); + when(refreshClock.instant()).thenReturn(NOW); + when(refreshStrategy.shouldBeRefreshed(any())).thenReturn(true); + + // set default expiration values + tokenBuilder + .expiresIn(5, MINUTES) + .refreshableFor(10, MINUTES); + } + + @Test + public void shouldNotRefreshTokenWithDisabledRefresh() { + JwtAccessToken oldToken = tokenBuilder + .refreshableFor(0, MINUTES) + .build(); + + Optional refreshedToken = refresher.refresh(oldToken); + + assertThat(refreshedToken).isEmpty(); + } + + @Test + public void shouldNotRefreshTokenWhenTokenExpired() { + Instant afterNormalExpiration = NOW.plus(ofMinutes(6)); + when(refreshClock.instant()).thenReturn(afterNormalExpiration); + JwtAccessToken oldToken = tokenBuilder.build(); + + Optional refreshedToken = refresher.refresh(oldToken); + + assertThat(refreshedToken).isEmpty(); + } + + @Test + public void shouldNotRefreshTokenWhenRefreshExpired() { + Instant afterRefreshExpiration = Instant.now().plus(ofMinutes(2)); + when(refreshClock.instant()).thenReturn(afterRefreshExpiration); + JwtAccessToken oldToken = tokenBuilder + .refreshableFor(1, MINUTES) + .build(); + + Optional refreshedToken = refresher.refresh(oldToken); + + assertThat(refreshedToken).isEmpty(); + } + + @Test + public void shouldNotRefreshTokenWhenStrategyDoesNotSaySo() { + JwtAccessToken oldToken = tokenBuilder.build(); + when(refreshStrategy.shouldBeRefreshed(oldToken)).thenReturn(false); + + Optional refreshedToken = refresher.refresh(oldToken); + + assertThat(refreshedToken).isEmpty(); + } + + @Test + public void shouldRefreshTokenWithParentId() { + JwtAccessToken oldToken = tokenBuilder.build(); + when(refreshStrategy.shouldBeRefreshed(oldToken)).thenReturn(true); + + Optional refreshedTokenResult = refresher.refresh(oldToken); + + assertThat(refreshedTokenResult).isNotEmpty(); + JwtAccessToken refreshedToken = refreshedTokenResult.get(); + assertThat(refreshedToken.getParentKey()).get().isEqualTo("key"); + } + + @Test + public void shouldRefreshTokenWithSameExpiration() { + JwtAccessToken oldToken = tokenBuilder.build(); + when(refreshStrategy.shouldBeRefreshed(oldToken)).thenReturn(true); + + Optional refreshedTokenResult = refresher.refresh(oldToken); + + assertThat(refreshedTokenResult).isNotEmpty(); + JwtAccessToken refreshedToken = refreshedTokenResult.get(); + assertThat(refreshedToken.getExpiration()).isEqualTo(Date.from(NOW.plus(ofMinutes(5)))); + } + + @Test + public void shouldRefreshTokenWithSameRefreshExpiration() { + JwtAccessToken oldToken = tokenBuilder.build(); + when(refreshStrategy.shouldBeRefreshed(oldToken)).thenReturn(true); + + Optional refreshedTokenResult = refresher.refresh(oldToken); + + assertThat(refreshedTokenResult).isNotEmpty(); + JwtAccessToken refreshedToken = refreshedTokenResult.get(); + assertThat(refreshedToken.getRefreshExpiration()).get().isEqualTo(Date.from(TOKEN_CREATION.plus(ofMinutes(10)))); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenResolverTest.java b/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenResolverTest.java index 689fc4bb35..d4341f104e 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenResolverTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenResolverTest.java @@ -56,6 +56,8 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; import static org.mockito.Mockito.*; +import static sonia.scm.security.SecureKeyTestUtil.createSecureKey; + import org.mockito.junit.MockitoJUnitRunner; /** @@ -214,12 +216,6 @@ public class JwtAccessTokenResolverTest { .compact(); } - private SecureKey createSecureKey() { - byte[] bytes = new byte[32]; - random.nextBytes(bytes); - return new SecureKey(bytes, System.currentTimeMillis()); - } - private void resolveKey(SecureKey key) { when( keyResolver.resolveSigningKey( @@ -230,7 +226,7 @@ public class JwtAccessTokenResolverTest { .thenReturn( new SecretKeySpec( key.getBytes(), - SignatureAlgorithm.HS256.getValue() + SignatureAlgorithm.HS256.getJcaName() ) ); } diff --git a/scm-webapp/src/test/java/sonia/scm/security/PercentageJwtAccessTokenRefreshStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/security/PercentageJwtAccessTokenRefreshStrategyTest.java new file mode 100644 index 0000000000..122c1b5381 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/security/PercentageJwtAccessTokenRefreshStrategyTest.java @@ -0,0 +1,67 @@ +package sonia.scm.security; + +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.time.Clock; +import java.time.Instant; +import java.util.Collections; + +import static java.time.temporal.ChronoUnit.MINUTES; +import static java.time.temporal.ChronoUnit.SECONDS; +import static java.util.concurrent.TimeUnit.HOURS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static sonia.scm.security.SecureKeyTestUtil.createSecureKey; + +@SubjectAware( + username = "user", + password = "secret", + configuration = "classpath:sonia/scm/repository/shiro.ini" +) +public class PercentageJwtAccessTokenRefreshStrategyTest { + + private static final Instant TOKEN_CREATION = Instant.now().truncatedTo(SECONDS); + + @Rule + public ShiroRule shiro = new ShiroRule(); + + private KeyGenerator keyGenerator = () -> "key"; + + private Clock refreshClock = mock(Clock.class); + + private JwtAccessTokenBuilder tokenBuilder; + private PercentageJwtAccessTokenRefreshStrategy refreshStrategy; + + @Before + public void initToken() { + SecureKeyResolver keyResolver = mock(SecureKeyResolver.class); + when(keyResolver.getSecureKey(any())).thenReturn(createSecureKey()); + + Clock creationClock = mock(Clock.class); + when(creationClock.instant()).thenReturn(TOKEN_CREATION); + + tokenBuilder = new JwtAccessTokenBuilderFactory(keyGenerator, keyResolver, Collections.emptySet(), creationClock).create(); + tokenBuilder.expiresIn(1, HOURS); + tokenBuilder.refreshableFor(1, HOURS); + + refreshStrategy = new PercentageJwtAccessTokenRefreshStrategy(refreshClock, 0.5F); + } + + @Test + public void shouldNotRefreshWhenTokenIsYoung() { + when(refreshClock.instant()).thenReturn(TOKEN_CREATION.plus(29, MINUTES)); + assertThat(refreshStrategy.shouldBeRefreshed(tokenBuilder.build())).isFalse(); + } + + @Test + public void shouldRefreshWhenTokenIsOld() { + when(refreshClock.instant()).thenReturn(TOKEN_CREATION.plus(31, MINUTES)); + assertThat(refreshStrategy.shouldBeRefreshed(tokenBuilder.build())).isTrue(); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/security/SecureKeyResolverTest.java b/scm-webapp/src/test/java/sonia/scm/security/SecureKeyResolverTest.java index 8d708c4677..c4f281537e 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/SecureKeyResolverTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/SecureKeyResolverTest.java @@ -36,20 +36,22 @@ package sonia.scm.security; //~--- non-JDK imports -------------------------------------------------------- import io.jsonwebtoken.Jwts; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; - import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; - import sonia.scm.store.ConfigurationEntryStore; import sonia.scm.store.ConfigurationEntryStoreFactory; -import static org.junit.Assert.*; - -import static org.mockito.Mockito.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * @@ -122,11 +124,14 @@ public class SecureKeyResolverTest @Before public void setUp() { - ConfigurationEntryStoreFactory factory = - mock(ConfigurationEntryStoreFactory.class); + ConfigurationEntryStoreFactory factory = mock(ConfigurationEntryStoreFactory.class); - when(factory.getStore(SecureKey.class, - SecureKeyResolver.STORE_NAME)).thenReturn(store); + when(factory.withType(any())).thenCallRealMethod(); + when(factory.getStore(argThat(storeParameters -> { + assertThat(storeParameters.getName()).isEqualTo(SecureKeyResolver.STORE_NAME); + assertThat(storeParameters.getType()).isEqualTo(SecureKey.class); + return true; + }))).thenReturn(store); resolver = new SecureKeyResolver(factory); } diff --git a/scm-webapp/src/test/java/sonia/scm/security/SecureKeyTestUtil.java b/scm-webapp/src/test/java/sonia/scm/security/SecureKeyTestUtil.java new file mode 100644 index 0000000000..3b9c95fd17 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/security/SecureKeyTestUtil.java @@ -0,0 +1,11 @@ +package sonia.scm.security; + +import java.security.SecureRandom; + +public class SecureKeyTestUtil { + public static SecureKey createSecureKey() { + byte[] bytes = new byte[32]; + new SecureRandom().nextBytes(bytes); + return new SecureKey(bytes, System.currentTimeMillis()); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/user/DefaultUserManagerTest.java b/scm-webapp/src/test/java/sonia/scm/user/DefaultUserManagerTest.java index 1614bf790a..8e261b75cc 100644 --- a/scm-webapp/src/test/java/sonia/scm/user/DefaultUserManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/user/DefaultUserManagerTest.java @@ -45,6 +45,9 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; import sonia.scm.NotFoundException; +import sonia.scm.repository.InitialRepositoryLocationResolver; +import sonia.scm.repository.RepositoryDAO; +import sonia.scm.repository.RepositoryLocationResolver; import sonia.scm.store.JAXBConfigurationStoreFactory; import sonia.scm.user.xml.XmlUserDAO; @@ -72,7 +75,7 @@ public class DefaultUserManagerTest extends UserManagerTestBase public ShiroRule shiro = new ShiroRule(); - private UserDAO userDAO = mock(UserDAO.class); + private UserDAO userDAO ; private User trillian; /** @@ -182,6 +185,6 @@ public class DefaultUserManagerTest extends UserManagerTestBase //~--- methods -------------------------------------------------------------- private XmlUserDAO createXmlUserDAO() { - return new XmlUserDAO(new JAXBConfigurationStoreFactory(contextProvider)); + return new XmlUserDAO(new JAXBConfigurationStoreFactory(contextProvider, locationResolver)); } } diff --git a/scm-webapp/src/test/java/sonia/scm/web/security/TokenRefreshFilterTest.java b/scm-webapp/src/test/java/sonia/scm/web/security/TokenRefreshFilterTest.java new file mode 100644 index 0000000000..945d8cf0d2 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/web/security/TokenRefreshFilterTest.java @@ -0,0 +1,107 @@ +package sonia.scm.web.security; + +import org.apache.shiro.authc.AuthenticationToken; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.security.AccessTokenCookieIssuer; +import sonia.scm.security.AccessTokenResolver; +import sonia.scm.security.BearerToken; +import sonia.scm.security.JwtAccessToken; +import sonia.scm.security.JwtAccessTokenRefresher; +import sonia.scm.web.WebTokenGenerator; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Set; + +import static java.util.Collections.singleton; +import static java.util.Optional.of; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith({MockitoExtension.class}) +class TokenRefreshFilterTest { + + @Mock + private Set tokenGenerators; + @Mock + private WebTokenGenerator tokenGenerator; + @Mock + private JwtAccessTokenRefresher refresher; + @Mock + private AccessTokenResolver resolver; + @Mock + private AccessTokenCookieIssuer issuer; + + @InjectMocks + private TokenRefreshFilter filter; + + @Mock + private HttpServletRequest request; + @Mock + private HttpServletResponse response; + @Mock + private FilterChain filterChain; + + @BeforeEach + void initGenerators() { + when(tokenGenerators.iterator()).thenReturn(singleton(tokenGenerator).iterator()); + } + + @Test + void shouldContinueChain() throws IOException, ServletException { + filter.doFilter(request, response, filterChain); + + verify(filterChain).doFilter(request, response); + verify(issuer, never()).authenticate(any(), any(), any()); + } + + @Test + void shouldNotRefreshNonBearerToken() throws IOException, ServletException { + AuthenticationToken token = mock(AuthenticationToken.class); + when(tokenGenerator.createToken(request)).thenReturn(token); + + filter.doFilter(request, response, filterChain); + + verify(issuer, never()).authenticate(any(), any(), any()); + verify(filterChain).doFilter(request, response); + } + + @Test + void shouldNotRefreshNonJwtToken() throws IOException, ServletException { + BearerToken token = mock(BearerToken.class); + JwtAccessToken jwtToken = mock(JwtAccessToken.class); + when(tokenGenerator.createToken(request)).thenReturn(token); + when(resolver.resolve(token)).thenReturn(jwtToken); + + filter.doFilter(request, response, filterChain); + + verify(issuer, never()).authenticate(any(), any(), any()); + verify(filterChain).doFilter(request, response); + } + + @Test + void shouldRefreshIfRefreshable() throws IOException, ServletException { + BearerToken token = mock(BearerToken.class); + JwtAccessToken jwtToken = mock(JwtAccessToken.class); + JwtAccessToken newJwtToken = mock(JwtAccessToken.class); + when(tokenGenerator.createToken(request)).thenReturn(token); + when(resolver.resolve(token)).thenReturn(jwtToken); + when(refresher.refresh(jwtToken)).thenReturn(of(newJwtToken)); + + filter.doFilter(request, response, filterChain); + + verify(issuer).authenticate(request, response, newJwtToken); + verify(filterChain).doFilter(request, response); + } +} diff --git a/scm-webapp/src/test/resources/sonia/scm/repository/shiro.ini b/scm-webapp/src/test/resources/sonia/scm/repository/shiro.ini index 9a39a2d46c..500325faf3 100644 --- a/scm-webapp/src/test/resources/sonia/scm/repository/shiro.ini +++ b/scm-webapp/src/test/resources/sonia/scm/repository/shiro.ini @@ -4,6 +4,7 @@ dent = secret, creator, heartOfGold, puzzle42 unpriv = secret crato = secret, creator community = secret, oss +user = secret, user [roles] admin = * @@ -11,3 +12,4 @@ creator = repository:create heartOfGold = "repository:read,modify,delete:hof" puzzle42 = "repository:read,write:p42" oss = "repository:pull" +user = *