mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-02-03 05:09:10 +01:00
merge repository heads
This commit is contained in:
3
Jenkinsfile
vendored
3
Jenkinsfile
vendored
@@ -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()) {
|
||||
|
||||
7
pom.xml
7
pom.xml
@@ -810,6 +810,13 @@
|
||||
<netbeans.hint.license>SCM-BSD</netbeans.hint.license>
|
||||
<jdk.classifier />
|
||||
<org.mapstruct.version>1.2.0.Final</org.mapstruct.version>
|
||||
|
||||
<!-- Sonar exclusions -->
|
||||
<!-- *StoreFactory classes are excluded because extracting the floating store parameter classes in a generic -->
|
||||
<!-- common class creates runtime errors (IncompatibleClassChange) -->
|
||||
<!-- *UserPassword JS files are excluded because extraction of common code would not make the code more readable -->
|
||||
<sonar.cpd.exclusions>**/*StoreFactory.java,**/*UserPassword.js</sonar.cpd.exclusions>
|
||||
|
||||
</properties>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -221,5 +221,5 @@
|
||||
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
||||
</project>
|
||||
|
||||
@@ -72,9 +72,11 @@ public abstract class AbstractRepositoryHandler<C extends RepositoryConfig>
|
||||
*
|
||||
* @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 ----------------------------------------------------------
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<String> 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<Date> 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 <T> type of field
|
||||
* @param key key of token field
|
||||
*
|
||||
*
|
||||
* @return optional value of custom field
|
||||
*/
|
||||
<T> Optional<T> 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<String, Object> getClaims();
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
* <br>
|
||||
* You can either create a global {@link BlobStore} or a {@link BlobStore} for a specific repository. To create a global
|
||||
* {@link BlobStore} call:
|
||||
* <code><pre>
|
||||
* blobStoreFactory
|
||||
* .withName("name")
|
||||
* .build();
|
||||
* </pre></code>
|
||||
* To create a {@link BlobStore} for a specific repository call:
|
||||
* <code><pre>
|
||||
* blobStoreFactory
|
||||
* .withName("name")
|
||||
* .forRepository(repository)
|
||||
* .build();
|
||||
* </pre></code>
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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. <b>Note:</b> 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.
|
||||
* <br>
|
||||
* <b>Note:</b> 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.
|
||||
* <br>
|
||||
* You can either create a global {@link ConfigurationEntryStore} or a {@link ConfigurationEntryStore} for a specific
|
||||
* repository. To create a global {@link ConfigurationEntryStore} call:
|
||||
* <code><pre>
|
||||
* configurationEntryStoreFactory
|
||||
* .withType(PersistedType.class)
|
||||
* .withName("name")
|
||||
* .build();
|
||||
* </pre></code>
|
||||
* To create a {@link ConfigurationEntryStore} for a specific repository call:
|
||||
* <code><pre>
|
||||
* configurationEntryStoreFactory
|
||||
* .withType(PersistedType.class)
|
||||
* .withName("name")
|
||||
* .forRepository(repository)
|
||||
* .build();
|
||||
* </pre></code>
|
||||
*
|
||||
* @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 <T> 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 <T> ConfigurationEntryStore<T> getStore(Class<T> type, String name);
|
||||
<T> ConfigurationEntryStore<T> getStore(final TypedStoreParameters<T> 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 <T> TypedFloatingConfigurationEntryStoreParameters<T>.Builder withType(Class<T> type) {
|
||||
return new TypedFloatingConfigurationEntryStoreParameters<T>(this).new Builder(type);
|
||||
}
|
||||
}
|
||||
|
||||
final class TypedFloatingConfigurationEntryStoreParameters<T> {
|
||||
|
||||
private final TypedStoreParametersImpl<T> parameters = new TypedStoreParametersImpl<>();
|
||||
private final ConfigurationEntryStoreFactory factory;
|
||||
|
||||
TypedFloatingConfigurationEntryStoreParameters(ConfigurationEntryStoreFactory factory) {
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
public class Builder {
|
||||
|
||||
Builder(Class<T> 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<T> build(){
|
||||
return factory.getStore(parameters);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
* <br>
|
||||
* <b>Note:</b> 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.
|
||||
* <br>
|
||||
* You can either create a global {@link ConfigurationStore} or a {@link ConfigurationStore} for a specific repository.
|
||||
* To create a global {@link ConfigurationStore} call:
|
||||
* <code><pre>
|
||||
* configurationStoreFactory
|
||||
* .withType(PersistedType.class)
|
||||
* .withName("name")
|
||||
* .build();
|
||||
* </pre></code>
|
||||
* To create a {@link ConfigurationStore} for a specific repository call:
|
||||
* <code><pre>
|
||||
* configurationStoreFactory
|
||||
* .withType(PersistedType.class)
|
||||
* .withName("name")
|
||||
* .forRepository(repository)
|
||||
* .build();
|
||||
* </pre></code>
|
||||
*
|
||||
* @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 <T> 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 <T> ConfigurationStore<T> getStore(Class<T> type, String name);
|
||||
<T> ConfigurationStore<T> getStore(final TypedStoreParameters<T> 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 <T> TypedFloatingConfigurationStoreParameters<T>.Builder withType(Class<T> type) {
|
||||
return new TypedFloatingConfigurationStoreParameters<T>(this).new Builder(type);
|
||||
}
|
||||
}
|
||||
|
||||
final class TypedFloatingConfigurationStoreParameters<T> {
|
||||
|
||||
private final TypedStoreParametersImpl<T> parameters = new TypedStoreParametersImpl<>();
|
||||
private final ConfigurationStoreFactory factory;
|
||||
|
||||
TypedFloatingConfigurationStoreParameters(ConfigurationStoreFactory factory) {
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
public class Builder {
|
||||
|
||||
Builder(Class<T> 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<T> build(){
|
||||
return factory.getStore(parameters);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
* <br>
|
||||
* You can either create a global {@link DataStore} or a {@link DataStore} for a specific repository.
|
||||
* To create a global {@link DataStore} call:
|
||||
* <code><pre>
|
||||
* dataStoreFactory
|
||||
* .withType(PersistedType.class)
|
||||
* .withName("name")
|
||||
* .build();
|
||||
* </pre></code>
|
||||
* To create a {@link DataStore} for a specific repository call:
|
||||
* <code><pre>
|
||||
* dataStoreFactory
|
||||
* .withType(PersistedType.class)
|
||||
* .withName("name")
|
||||
* .forRepository(repository)
|
||||
* .build();
|
||||
* </pre></code>
|
||||
*
|
||||
* @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 <T> 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 <T> DataStore<T> getStore(Class<T> type, String name);
|
||||
<T> DataStore<T> getStore(final TypedStoreParameters<T> 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 <T> TypedFloatingDataStoreParameters<T>.Builder withType(Class<T> type) {
|
||||
return new TypedFloatingDataStoreParameters<T>(this).new Builder(type);
|
||||
}
|
||||
}
|
||||
|
||||
final class TypedFloatingDataStoreParameters<T> {
|
||||
|
||||
private final TypedStoreParametersImpl<T> parameters = new TypedStoreParametersImpl<>();
|
||||
private final DataStoreFactory factory;
|
||||
|
||||
TypedFloatingDataStoreParameters(DataStoreFactory factory) {
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
public class Builder {
|
||||
|
||||
Builder(Class<T> 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<T> build(){
|
||||
return factory.getStore(parameters);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
16
scm-core/src/main/java/sonia/scm/store/StoreParameters.java
Normal file
16
scm-core/src/main/java/sonia/scm/store/StoreParameters.java
Normal file
@@ -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();
|
||||
}
|
||||
@@ -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<T> {
|
||||
|
||||
Class<T> getType();
|
||||
|
||||
String getName();
|
||||
|
||||
Repository getRepository();
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package sonia.scm.store;
|
||||
|
||||
import sonia.scm.repository.Repository;
|
||||
|
||||
class TypedStoreParametersImpl<T> implements TypedStoreParameters<T> {
|
||||
private Class<T> type;
|
||||
private String name;
|
||||
private Repository repository;
|
||||
|
||||
@Override
|
||||
public Class<T> getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
void setType(Class<T> 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;
|
||||
}
|
||||
}
|
||||
@@ -64,9 +64,11 @@ public class XmlGroupDAO extends AbstractXmlDAO<Group, XmlGroupDatabase>
|
||||
* @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 --------------------------------------------------------------
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) 2010, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* <p>
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* <p>
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
@@ -24,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.
|
||||
*
|
||||
* <p>
|
||||
* 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 <T>
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public <T> ConfigurationEntryStore<T> getStore(Class<T> type, String name)
|
||||
{
|
||||
logger.debug("create new configuration store for type {} with name {}",
|
||||
type, name);
|
||||
|
||||
//J-
|
||||
return new JAXBConfigurationEntryStore<T>(
|
||||
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 <T> ConfigurationEntryStore<T> getStore(TypedStoreParameters<T> storeParameters) {
|
||||
return new JAXBConfigurationEntryStore<>(getStoreLocation(storeParameters.getName().concat(StoreConstants.FILE_EXTENSION), storeParameters.getType(), storeParameters.getRepository()), keyGenerator, storeParameters.getType());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ public class JAXBConfigurationStore<T> extends AbstractStore<T> {
|
||||
|
||||
private JAXBContext context;
|
||||
|
||||
JAXBConfigurationStore(Class<T> type, File configFile) {
|
||||
public JAXBConfigurationStore(Class<T> type, File configFile) {
|
||||
this.type = type;
|
||||
|
||||
try {
|
||||
|
||||
@@ -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 <T> JAXBConfigurationStore<T> getStore(Class<T> 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 <T> JAXBConfigurationStore<T> getStore(TypedStoreParameters<T> storeParameters) {
|
||||
return new JAXBConfigurationStore<>(storeParameters.getType(), getStoreLocation(storeParameters.getName().concat(StoreConstants.FILE_EXTENSION), storeParameters.getType(), storeParameters.getRepository()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 <T>
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public <T> DataStore<T> getStore(Class<T> type, String name)
|
||||
{
|
||||
logger.debug("create new store for type {} with name {}", type, name);
|
||||
|
||||
return new JAXBDataStore<>(keyGenerator, type, getDirectory(name));
|
||||
public <T> DataStore<T> getStore(TypedStoreParameters<T> storeParameters) {
|
||||
File storeLocation = getStoreLocation(storeParameters);
|
||||
IOUtil.mkdirs(storeLocation);
|
||||
return new JAXBDataStore<>(keyGenerator, storeParameters.getType(), storeLocation);
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** Field description */
|
||||
private KeyGenerator keyGenerator;
|
||||
}
|
||||
|
||||
49
scm-dao-xml/src/main/java/sonia/scm/store/Store.java
Normal file
49
scm-dao-xml/src/main/java/sonia/scm/store/Store.java
Normal file
@@ -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
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -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<User, XmlUserDatabase>
|
||||
@Inject
|
||||
public XmlUserDAO(ConfigurationStoreFactory storeFactory)
|
||||
{
|
||||
super(storeFactory.getStore(XmlUserDatabase.class, STORE_NAME));
|
||||
super(storeFactory
|
||||
.withType(XmlUserDatabase.class)
|
||||
.withName(STORE_NAME)
|
||||
.build());
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<Blob> storedBlobs = store.getAll();
|
||||
|
||||
assertNotNull(createdBlob);
|
||||
assertThat(storedBlobs)
|
||||
.isNotNull()
|
||||
.hasSize(1)
|
||||
.usingElementComparatorOnFields("id").containsExactly(createdBlob);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<AssignedPermission> store =
|
||||
createPermissionStore(RESOURCE_FIXED, name);
|
||||
ConfigurationEntryStore<AssignedPermission> 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<StoreObject> 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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<RepositoryManager> 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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<T extends ModelObject>
|
||||
public TemporaryFolder tempFolder = new TemporaryFolder();
|
||||
|
||||
protected SCMContextProvider contextProvider;
|
||||
|
||||
protected RepositoryLocationResolver locationResolver;
|
||||
|
||||
protected Manager<T> 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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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<StoreObject> 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<StoreObject> 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<StoreObject> store = dataStoreFactory
|
||||
.withType(StoreObject.class)
|
||||
.withName("test")
|
||||
.forRepository(repository)
|
||||
.build();
|
||||
|
||||
String id = store.put(obj);
|
||||
|
||||
assertNotNull(id);
|
||||
|
||||
assertEquals(obj, store.get(id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,8 +43,7 @@ package sonia.scm.store;
|
||||
public class InMemoryConfigurationStoreFactory implements ConfigurationStoreFactory {
|
||||
|
||||
@Override
|
||||
public <T> ConfigurationStore<T> getStore(Class<T> type, String name)
|
||||
{
|
||||
public ConfigurationStore getStore(TypedStoreParameters storeParameters) {
|
||||
return new InMemoryConfigurationStore<>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<StoreObject> store;
|
||||
protected DataStore<StoreObject> repoStore;
|
||||
protected String repoStoreName = "testRepoStore";
|
||||
protected String storeName = "testStore";
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
protected abstract DataStore<StoreObject> getDataStore();
|
||||
protected abstract <STORE_OBJECT> DataStore<STORE_OBJECT> getDataStore(Class<STORE_OBJECT> type , Repository repository);
|
||||
protected abstract <STORE_OBJECT> DataStore<STORE_OBJECT> getDataStore(Class<STORE_OBJECT> 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<StoreObject> store;
|
||||
}
|
||||
|
||||
@@ -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<StoreObject> store = createStoreFactory().getStore(StoreObject.class,
|
||||
"test");
|
||||
ConfigurationStore<StoreObject> 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<StoreObject> store = createStoreFactory().getStore(StoreObject.class,
|
||||
"test");
|
||||
ConfigurationStore<StoreObject> store = createStoreFactory().withType(StoreObject.class).withName("test").build();
|
||||
|
||||
assertNotNull(store);
|
||||
|
||||
|
||||
@@ -72,8 +72,18 @@
|
||||
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt</artifactId>
|
||||
<version>0.4</version>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>${jjwt.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>${jjwt.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>${jjwt.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- json -->
|
||||
@@ -540,6 +550,7 @@
|
||||
<scm.stage>DEVELOPMENT</scm.stage>
|
||||
<scm.home>target/scm-it</scm.home>
|
||||
<environment.profile>default</environment.profile>
|
||||
<jjwt.version>0.10.5</jjwt.version>
|
||||
<selenium.version>2.53.1</selenium.version>
|
||||
<wagon.version>1.0</wagon.version>
|
||||
<mustache.version>0.8.17</mustache.version>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -52,7 +52,7 @@ public final class DebugService
|
||||
private final Multimap<NamespaceAndName,DebugHookData> receivedHooks = LinkedListMultimap.create();
|
||||
|
||||
/**
|
||||
* Stores {@link DebugHookData} for the given repository.
|
||||
* Store {@link DebugHookData} for the given repository.
|
||||
*/
|
||||
void put(NamespaceAndName namespaceAndName, DebugHookData hookData)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package sonia.scm.security;
|
||||
|
||||
import sonia.scm.plugin.Extension;
|
||||
|
||||
@Extension
|
||||
public class DefaultJwtAccessTokenRefreshStrategy extends PercentageJwtAccessTokenRefreshStrategy {
|
||||
public DefaultJwtAccessTokenRefreshStrategy() {
|
||||
super(0.5F);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Date> getRefreshExpiration() {
|
||||
return ofNullable(claims.get(REFRESHABLE_UNTIL_CLAIM_KEY, Date.class));
|
||||
}
|
||||
|
||||
public Optional<String> 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<String, Object> getClaims() {
|
||||
return Collections.unmodifiableMap(claims);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String,Object> 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<String,Object> 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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<AccessTokenEnricher> enrichers;
|
||||
private final Clock clock;
|
||||
|
||||
@Inject
|
||||
public JwtAccessTokenBuilderFactory(
|
||||
KeyGenerator keyGenerator, SecureKeyResolver keyResolver, Set<AccessTokenEnricher> enrichers
|
||||
) {
|
||||
KeyGenerator keyGenerator, SecureKeyResolver keyResolver, Set<AccessTokenEnricher> enrichers) {
|
||||
this(keyGenerator, keyResolver, enrichers, Clock.systemDefaultZone());
|
||||
}
|
||||
|
||||
JwtAccessTokenBuilderFactory(
|
||||
KeyGenerator keyGenerator, SecureKeyResolver keyResolver, Set<AccessTokenEnricher> 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) -> {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package sonia.scm.security;
|
||||
|
||||
import sonia.scm.plugin.ExtensionPoint;
|
||||
|
||||
@ExtensionPoint(multi = false)
|
||||
public interface JwtAccessTokenRefreshStrategy {
|
||||
boolean shouldBeRefreshed(JwtAccessToken oldToken);
|
||||
}
|
||||
@@ -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<JwtAccessToken> refresh(JwtAccessToken oldToken) {
|
||||
JwtAccessTokenBuilder builder = builderFactory.create();
|
||||
Map<String, Object> claims = oldToken.getClaims();
|
||||
claims.forEach(builder::custom);
|
||||
|
||||
if (canBeRefreshed(oldToken) && shouldBeRefreshed(oldToken)) {
|
||||
Optional<Object> 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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 --------------------------------------------------------------
|
||||
|
||||
@@ -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<WebTokenGenerator> tokenGenerators;
|
||||
private final JwtAccessTokenRefresher refresher;
|
||||
private final AccessTokenResolver resolver;
|
||||
private final AccessTokenCookieIssuer issuer;
|
||||
|
||||
@Inject
|
||||
public TokenRefreshFilter(Set<WebTokenGenerator> 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<BearerToken> 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);
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,8 @@ public class AutoCompleteResourceTest {
|
||||
ConfigurationStore<Object> 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);
|
||||
|
||||
@@ -418,10 +418,10 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> {
|
||||
private DefaultRepositoryManager createRepositoryManager(boolean archiveEnabled, KeyGenerator keyGenerator) {
|
||||
DefaultFileSystem fileSystem = new DefaultFileSystem();
|
||||
Set<RepositoryHandler> 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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<JwtAccessToken> 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<JwtAccessToken> 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<JwtAccessToken> refreshedToken = refresher.refresh(oldToken);
|
||||
|
||||
assertThat(refreshedToken).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotRefreshTokenWhenStrategyDoesNotSaySo() {
|
||||
JwtAccessToken oldToken = tokenBuilder.build();
|
||||
when(refreshStrategy.shouldBeRefreshed(oldToken)).thenReturn(false);
|
||||
|
||||
Optional<JwtAccessToken> refreshedToken = refresher.refresh(oldToken);
|
||||
|
||||
assertThat(refreshedToken).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldRefreshTokenWithParentId() {
|
||||
JwtAccessToken oldToken = tokenBuilder.build();
|
||||
when(refreshStrategy.shouldBeRefreshed(oldToken)).thenReturn(true);
|
||||
|
||||
Optional<JwtAccessToken> 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<JwtAccessToken> 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<JwtAccessToken> refreshedTokenResult = refresher.refresh(oldToken);
|
||||
|
||||
assertThat(refreshedTokenResult).isNotEmpty();
|
||||
JwtAccessToken refreshedToken = refreshedTokenResult.get();
|
||||
assertThat(refreshedToken.getRefreshExpiration()).get().isEqualTo(Date.from(TOKEN_CREATION.plus(ofMinutes(10))));
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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.<SecureKey>getStore(argThat(storeParameters -> {
|
||||
assertThat(storeParameters.getName()).isEqualTo(SecureKeyResolver.STORE_NAME);
|
||||
assertThat(storeParameters.getType()).isEqualTo(SecureKey.class);
|
||||
return true;
|
||||
}))).thenReturn(store);
|
||||
resolver = new SecureKeyResolver(factory);
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<WebTokenGenerator> 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);
|
||||
}
|
||||
}
|
||||
@@ -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 = *
|
||||
|
||||
Reference in New Issue
Block a user