merge with develop

This commit is contained in:
Eduard Heimbuch
2020-10-19 13:34:54 +02:00
34 changed files with 736 additions and 220 deletions

View File

@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased ## Unreleased
### Added
- Generation of email addresses for users, where none is configured ([#1370](https://github.com/scm-manager/scm-manager/pull/1370))
## [2.7.1] - 2020-10-14
### Fixed ### Fixed
- Null Pointer Exception on anonymous migration with deleted repositories ([#1371](https://github.com/scm-manager/scm-manager/pull/1371)) - Null Pointer Exception on anonymous migration with deleted repositories ([#1371](https://github.com/scm-manager/scm-manager/pull/1371))
- Null Pointer Exception on parsing SVN properties ([#1373](https://github.com/scm-manager/scm-manager/pull/1373)) - Null Pointer Exception on parsing SVN properties ([#1373](https://github.com/scm-manager/scm-manager/pull/1373))

View File

@@ -11,6 +11,12 @@ title: Intellij IDEA Configuration
### Settings ### Settings
* Build, Execution, Deployment / Compiler
* Add runtime assertions for non-null-annotated methods and parameters (must be checked)
* Configure annotation ... (of "Add runtime assertions...")
* Nullable annotations: select (✓) `javax.annotation.Nullable`
* NotNull annotations: select (✓) `javax.annotation.Nonnull` and check Instrument
* Run Configurations / Edit Configuration * Run Configurations / Edit Configuration
* Add Maven * Add Maven
* Name: run-backend * Name: run-backend

View File

@@ -903,7 +903,7 @@
<properties> <properties>
<!-- test libraries --> <!-- test libraries -->
<mockito.version>3.5.11</mockito.version> <mockito.version>3.5.13</mockito.version>
<hamcrest.version>2.1</hamcrest.version> <hamcrest.version>2.1</hamcrest.version>
<junit.version>5.7.0</junit.version> <junit.version>5.7.0</junit.version>
@@ -913,7 +913,7 @@
<servlet.version>3.1.0</servlet.version> <servlet.version>3.1.0</servlet.version>
<jaxrs.version>2.1.1</jaxrs.version> <jaxrs.version>2.1.1</jaxrs.version>
<resteasy.version>4.5.7.Final</resteasy.version> <resteasy.version>4.5.8.Final</resteasy.version>
<jersey-client.version>1.19.4</jersey-client.version> <jersey-client.version>1.19.4</jersey-client.version>
<jackson.version>2.11.2</jackson.version> <jackson.version>2.11.2</jackson.version>
<guice.version>4.2.3</guice.version> <guice.version>4.2.3</guice.version>

View File

@@ -80,6 +80,14 @@ public class ScmConfiguration implements Configuration {
*/ */
public static final String DEFAULT_LOGIN_INFO_URL = "https://login-info.scm-manager.org/api/v1/login-info"; public static final String DEFAULT_LOGIN_INFO_URL = "https://login-info.scm-manager.org/api/v1/login-info";
/**
* Default e-mail domain name that will be used whenever we have to generate an e-mail address for a user that has no
* mail address configured.
*
* @since 2.8.0
*/
public static final String DEFAULT_MAIL_DOMAIN_NAME = "scm-manager.local";
/** /**
* Default plugin url from version 1.0 * Default plugin url from version 1.0
*/ */
@@ -195,6 +203,8 @@ public class ScmConfiguration implements Configuration {
@XmlElement(name = "login-info-url") @XmlElement(name = "login-info-url")
private String loginInfoUrl = DEFAULT_LOGIN_INFO_URL; private String loginInfoUrl = DEFAULT_LOGIN_INFO_URL;
@XmlElement(name = "mail-domain-name")
private String mailDomainName = DEFAULT_MAIL_DOMAIN_NAME;
/** /**
* Calls the {@link sonia.scm.ConfigChangedListener#configChanged(Object)} * Calls the {@link sonia.scm.ConfigChangedListener#configChanged(Object)}
@@ -235,6 +245,7 @@ public class ScmConfiguration implements Configuration {
this.namespaceStrategy = other.namespaceStrategy; this.namespaceStrategy = other.namespaceStrategy;
this.loginInfoUrl = other.loginInfoUrl; this.loginInfoUrl = other.loginInfoUrl;
this.releaseFeedUrl = other.releaseFeedUrl; this.releaseFeedUrl = other.releaseFeedUrl;
this.mailDomainName = other.mailDomainName;
this.enabledUserConverter = other.enabledUserConverter; this.enabledUserConverter = other.enabledUserConverter;
} }
@@ -300,6 +311,15 @@ public class ScmConfiguration implements Configuration {
return releaseFeedUrl; return releaseFeedUrl;
} }
/**
* Returns the mail domain, that will be used to create e-mail addresses for users without one whenever one is required.
* @return default mail domain
* @since 2.8.0
*/
public String getMailDomainName() {
return mailDomainName;
}
/** /**
* Returns a set of glob patterns for urls which should excluded from * Returns a set of glob patterns for urls which should excluded from
* proxy settings. * proxy settings.
@@ -491,6 +511,16 @@ public class ScmConfiguration implements Configuration {
this.releaseFeedUrl = releaseFeedUrl; this.releaseFeedUrl = releaseFeedUrl;
} }
/**
* Sets the mail host, that will be used to create e-mail addresses for users without one whenever one is required.
*
* @param mailDomainName The default mail domain to use
* @since 2.8.0
*/
public void setMailDomainName(String mailDomainName) {
this.mailDomainName = mailDomainName;
}
/** /**
* Set glob patterns for urls which are should be excluded from proxy * Set glob patterns for urls which are should be excluded from proxy
* settings. * settings.

View File

@@ -30,7 +30,9 @@ import sonia.scm.repository.spi.MergeCommand;
import sonia.scm.repository.spi.MergeCommandRequest; import sonia.scm.repository.spi.MergeCommandRequest;
import sonia.scm.repository.spi.MergeConflictResult; import sonia.scm.repository.spi.MergeConflictResult;
import sonia.scm.repository.util.AuthorUtil; import sonia.scm.repository.util.AuthorUtil;
import sonia.scm.user.EMail;
import javax.annotation.Nullable;
import java.util.Set; import java.util.Set;
/** /**
@@ -78,8 +80,12 @@ public class MergeCommandBuilder {
private final MergeCommand mergeCommand; private final MergeCommand mergeCommand;
private final MergeCommandRequest request = new MergeCommandRequest(); private final MergeCommandRequest request = new MergeCommandRequest();
MergeCommandBuilder(MergeCommand mergeCommand) { @Nullable
private final EMail eMail;
MergeCommandBuilder(MergeCommand mergeCommand, @Nullable EMail eMail) {
this.mergeCommand = mergeCommand; this.mergeCommand = mergeCommand;
this.eMail = eMail;
} }
/** /**
@@ -209,7 +215,7 @@ public class MergeCommandBuilder {
* @return The result of the merge. * @return The result of the merge.
*/ */
public MergeCommandResult executeMerge() { public MergeCommandResult executeMerge() {
AuthorUtil.setAuthorIfNotAvailable(request); AuthorUtil.setAuthorIfNotAvailable(request, eMail);
Preconditions.checkArgument(request.isValid(), "revision to merge and target revision is required"); Preconditions.checkArgument(request.isValid(), "revision to merge and target revision is required");
return mergeCommand.merge(request); return mergeCommand.merge(request);
} }

View File

@@ -35,8 +35,10 @@ import sonia.scm.repository.spi.ModifyCommand;
import sonia.scm.repository.spi.ModifyCommandRequest; import sonia.scm.repository.spi.ModifyCommandRequest;
import sonia.scm.repository.util.AuthorUtil; import sonia.scm.repository.util.AuthorUtil;
import sonia.scm.repository.work.WorkdirProvider; import sonia.scm.repository.work.WorkdirProvider;
import sonia.scm.user.EMail;
import sonia.scm.util.IOUtil; import sonia.scm.util.IOUtil;
import javax.annotation.Nullable;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@@ -51,7 +53,6 @@ import java.util.function.Consumer;
* default a {@link sonia.scm.AlreadyExistsException} will be thrown)</li> * default a {@link sonia.scm.AlreadyExistsException} will be thrown)</li>
* <li>modify existing files ({@link #modifyFile(String)}</li> * <li>modify existing files ({@link #modifyFile(String)}</li>
* <li>delete existing files ({@link #deleteFile(String)}</li> * <li>delete existing files ({@link #deleteFile(String)}</li>
* <li>move/rename existing files ({@link #moveFile(String, String)}</li>
* </ul> * </ul>
* *
* You can collect multiple changes before they are executed with a call to {@link #execute()}. * You can collect multiple changes before they are executed with a call to {@link #execute()}.
@@ -75,11 +76,15 @@ public class ModifyCommandBuilder {
private final ModifyCommand command; private final ModifyCommand command;
private final File workdir; private final File workdir;
@Nullable
private final EMail eMail;
private final ModifyCommandRequest request = new ModifyCommandRequest(); private final ModifyCommandRequest request = new ModifyCommandRequest();
ModifyCommandBuilder(ModifyCommand command, WorkdirProvider workdirProvider) { ModifyCommandBuilder(ModifyCommand command, WorkdirProvider workdirProvider, @Nullable EMail eMail) {
this.command = command; this.command = command;
this.workdir = workdirProvider.createNewWorkdir(); this.workdir = workdirProvider.createNewWorkdir();
this.eMail = eMail;
} }
/** /**
@@ -124,7 +129,7 @@ public class ModifyCommandBuilder {
* @return The revision of the new commit. * @return The revision of the new commit.
*/ */
public String execute() { public String execute() {
AuthorUtil.setAuthorIfNotAvailable(request); AuthorUtil.setAuthorIfNotAvailable(request, eMail);
try { try {
Preconditions.checkArgument(request.isValid(), "commit message and at least one request are required"); Preconditions.checkArgument(request.isValid(), "commit message and at least one request are required");
return command.execute(request); return command.execute(request);

View File

@@ -34,7 +34,9 @@ import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.spi.RepositoryServiceProvider; import sonia.scm.repository.spi.RepositoryServiceProvider;
import sonia.scm.repository.work.WorkdirProvider; import sonia.scm.repository.work.WorkdirProvider;
import sonia.scm.user.EMail;
import javax.annotation.Nullable;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.util.Set; import java.util.Set;
@@ -84,30 +86,36 @@ public final class RepositoryService implements Closeable {
private final PreProcessorUtil preProcessorUtil; private final PreProcessorUtil preProcessorUtil;
private final RepositoryServiceProvider provider; private final RepositoryServiceProvider provider;
private final Repository repository; private final Repository repository;
@SuppressWarnings("rawtypes") @SuppressWarnings({"rawtypes", "java:S3740"})
private final Set<ScmProtocolProvider> protocolProviders; private final Set<ScmProtocolProvider> protocolProviders;
private final WorkdirProvider workdirProvider; private final WorkdirProvider workdirProvider;
@Nullable
private final EMail eMail;
/** /**
* Constructs a new {@link RepositoryService}. This constructor should only * Constructs a new {@link RepositoryService}. This constructor should only
* be called from the {@link RepositoryServiceFactory}. * be called from the {@link RepositoryServiceFactory}.
* @param cacheManager cache manager * @param cacheManager cache manager
* @param provider implementation for {@link RepositoryServiceProvider} * @param provider implementation for {@link RepositoryServiceProvider}
* @param repository the repository * @param repository the repository
* @param workdirProvider * @param workdirProvider provider for workdirs
* @param eMail utility to compute email addresses if missing
*/ */
RepositoryService(CacheManager cacheManager, RepositoryService(CacheManager cacheManager,
RepositoryServiceProvider provider, Repository repository, RepositoryServiceProvider provider,
Repository repository,
PreProcessorUtil preProcessorUtil, PreProcessorUtil preProcessorUtil,
@SuppressWarnings("rawtypes") Set<ScmProtocolProvider> protocolProviders, @SuppressWarnings({"rawtypes", "java:S3740"}) Set<ScmProtocolProvider> protocolProviders,
WorkdirProvider workdirProvider WorkdirProvider workdirProvider,
) { @Nullable EMail eMail) {
this.cacheManager = cacheManager; this.cacheManager = cacheManager;
this.provider = provider; this.provider = provider;
this.repository = repository; this.repository = repository;
this.preProcessorUtil = preProcessorUtil; this.preProcessorUtil = preProcessorUtil;
this.protocolProviders = protocolProviders; this.protocolProviders = protocolProviders;
this.workdirProvider = workdirProvider; this.workdirProvider = workdirProvider;
this.eMail = eMail;
} }
/** /**
@@ -397,7 +405,7 @@ public final class RepositoryService implements Closeable {
LOG.debug("create merge command for repository {}", LOG.debug("create merge command for repository {}",
repository.getNamespaceAndName()); repository.getNamespaceAndName());
return new MergeCommandBuilder(provider.getMergeCommand()); return new MergeCommandBuilder(provider.getMergeCommand(), eMail);
} }
/** /**
@@ -418,7 +426,7 @@ public final class RepositoryService implements Closeable {
LOG.debug("create modify command for repository {}", LOG.debug("create modify command for repository {}",
repository.getNamespaceAndName()); repository.getNamespaceAndName());
return new ModifyCommandBuilder(provider.getModifyCommand(), workdirProvider); return new ModifyCommandBuilder(provider.getModifyCommand(), workdirProvider, eMail);
} }
/** /**
@@ -448,7 +456,7 @@ public final class RepositoryService implements Closeable {
.map(this::createProviderInstanceForRepository); .map(this::createProviderInstanceForRepository);
} }
@SuppressWarnings("rawtypes") @SuppressWarnings({"rawtypes", "java:S3740"})
private ScmProtocol createProviderInstanceForRepository(ScmProtocolProvider protocolProvider) { private ScmProtocol createProviderInstanceForRepository(ScmProtocolProvider protocolProvider) {
return protocolProvider.get(repository); return protocolProvider.get(repository);
} }

View File

@@ -42,7 +42,6 @@ import sonia.scm.cache.Cache;
import sonia.scm.cache.CacheManager; import sonia.scm.cache.CacheManager;
import sonia.scm.config.ScmConfiguration; import sonia.scm.config.ScmConfiguration;
import sonia.scm.event.ScmEventBus; import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.BranchCreatedEvent;
import sonia.scm.repository.ClearRepositoryCacheEvent; import sonia.scm.repository.ClearRepositoryCacheEvent;
import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.PostReceiveRepositoryHookEvent; import sonia.scm.repository.PostReceiveRepositoryHookEvent;
@@ -58,7 +57,9 @@ import sonia.scm.repository.work.WorkdirProvider;
import sonia.scm.security.PublicKeyCreatedEvent; import sonia.scm.security.PublicKeyCreatedEvent;
import sonia.scm.security.PublicKeyDeletedEvent; import sonia.scm.security.PublicKeyDeletedEvent;
import sonia.scm.security.ScmSecurityException; import sonia.scm.security.ScmSecurityException;
import sonia.scm.user.EMail;
import javax.annotation.Nullable;
import java.util.Set; import java.util.Set;
import static sonia.scm.ContextEntry.ContextBuilder.entity; import static sonia.scm.ContextEntry.ContextBuilder.entity;
@@ -115,7 +116,17 @@ public final class RepositoryServiceFactory {
private static final Logger logger = private static final Logger logger =
LoggerFactory.getLogger(RepositoryServiceFactory.class); LoggerFactory.getLogger(RepositoryServiceFactory.class);
//~--- constructors --------------------------------------------------------- private final CacheManager cacheManager;
private final RepositoryManager repositoryManager;
private final Set<RepositoryServiceResolver> resolvers;
private final PreProcessorUtil preProcessorUtil;
@SuppressWarnings({"rawtypes", "java:S3740"})
private final Set<ScmProtocolProvider> protocolProviders;
private final WorkdirProvider workdirProvider;
@Nullable
private final EMail eMail;
/** /**
* Constructs a new {@link RepositoryServiceFactory}. This constructor * Constructs a new {@link RepositoryServiceFactory}. This constructor
@@ -127,40 +138,67 @@ public final class RepositoryServiceFactory {
* @param repositoryManager manager for repositories * @param repositoryManager manager for repositories
* @param resolvers a set of {@link RepositoryServiceResolver} * @param resolvers a set of {@link RepositoryServiceResolver}
* @param preProcessorUtil helper object for pre processor handling * @param preProcessorUtil helper object for pre processor handling
* @param protocolProviders * @param protocolProviders providers for repository protocols
* @param workdirProvider * @param workdirProvider provider for working directories
*
* @deprecated use {@link RepositoryServiceFactory#RepositoryServiceFactory(CacheManager, RepositoryManager, Set, PreProcessorUtil, Set, WorkdirProvider, EMail)} instead
* @since 1.21 * @since 1.21
*/ */
@Inject @Deprecated
public RepositoryServiceFactory(ScmConfiguration configuration, public RepositoryServiceFactory(ScmConfiguration configuration,
CacheManager cacheManager, RepositoryManager repositoryManager, CacheManager cacheManager, RepositoryManager repositoryManager,
Set<RepositoryServiceResolver> resolvers, PreProcessorUtil preProcessorUtil, Set<RepositoryServiceResolver> resolvers, PreProcessorUtil preProcessorUtil,
@SuppressWarnings("rawtypes") Set<ScmProtocolProvider> protocolProviders, WorkdirProvider workdirProvider) { @SuppressWarnings({"rawtypes", "java:S3740"}) Set<ScmProtocolProvider> protocolProviders,
WorkdirProvider workdirProvider) {
this( this(
configuration, cacheManager, repositoryManager, resolvers, cacheManager, repositoryManager, resolvers,
preProcessorUtil, protocolProviders, workdirProvider, ScmEventBus.getInstance() preProcessorUtil, protocolProviders, workdirProvider, null, ScmEventBus.getInstance()
);
}
/**
* Constructs a new {@link RepositoryServiceFactory}. This constructor
* should not be called manually, it should only be used by the injection
* container.
*
* @param cacheManager cache manager
* @param repositoryManager manager for repositories
* @param resolvers a set of {@link RepositoryServiceResolver}
* @param preProcessorUtil helper object for pre processor handling
* @param protocolProviders providers for repository protocols
* @param workdirProvider provider for working directories
* @param eMail handling user emails
* @since 2.8.0
*/
@Inject
public RepositoryServiceFactory(CacheManager cacheManager, RepositoryManager repositoryManager,
Set<RepositoryServiceResolver> resolvers, PreProcessorUtil preProcessorUtil,
@SuppressWarnings({"rawtypes", "java:S3740"}) Set<ScmProtocolProvider> protocolProviders,
WorkdirProvider workdirProvider, EMail eMail) {
this(
cacheManager, repositoryManager, resolvers,
preProcessorUtil, protocolProviders, workdirProvider,
eMail, ScmEventBus.getInstance()
); );
} }
@VisibleForTesting @VisibleForTesting
RepositoryServiceFactory(ScmConfiguration configuration, @SuppressWarnings("java:S107") // to keep backward compatibility, we can not reduce amount of parameters
CacheManager cacheManager, RepositoryManager repositoryManager, RepositoryServiceFactory(CacheManager cacheManager, RepositoryManager repositoryManager,
Set<RepositoryServiceResolver> resolvers, PreProcessorUtil preProcessorUtil, Set<RepositoryServiceResolver> resolvers, PreProcessorUtil preProcessorUtil,
Set<ScmProtocolProvider> protocolProviders, WorkdirProvider workdirProvider, @SuppressWarnings({"rawtypes", "java:S3740"}) Set<ScmProtocolProvider> protocolProviders,
ScmEventBus eventBus) { WorkdirProvider workdirProvider, @Nullable EMail eMail, ScmEventBus eventBus) {
this.configuration = configuration;
this.cacheManager = cacheManager; this.cacheManager = cacheManager;
this.repositoryManager = repositoryManager; this.repositoryManager = repositoryManager;
this.resolvers = resolvers; this.resolvers = resolvers;
this.preProcessorUtil = preProcessorUtil; this.preProcessorUtil = preProcessorUtil;
this.protocolProviders = protocolProviders; this.protocolProviders = protocolProviders;
this.workdirProvider = workdirProvider; this.workdirProvider = workdirProvider;
this.eMail = eMail;
eventBus.register(new CacheClearHook(cacheManager)); eventBus.register(new CacheClearHook(cacheManager));
} }
//~--- methods --------------------------------------------------------------
/** /**
* Creates a new RepositoryService for the given repository. * Creates a new RepositoryService for the given repository.
* *
@@ -246,7 +284,7 @@ public final class RepositoryServiceFactory {
} }
service = new RepositoryService(cacheManager, provider, repository, service = new RepositoryService(cacheManager, provider, repository,
preProcessorUtil, protocolProviders, workdirProvider); preProcessorUtil, protocolProviders, workdirProvider, eMail);
break; break;
} }
@@ -259,8 +297,6 @@ public final class RepositoryServiceFactory {
return service; return service;
} }
//~--- inner classes --------------------------------------------------------
/** /**
* Hook and listener to clear all relevant repository caches. * Hook and listener to clear all relevant repository caches.
*/ */
@@ -284,8 +320,6 @@ public final class RepositoryServiceFactory {
this.caches.add(cacheManager.getCache(BranchesCommandBuilder.CACHE_NAME)); this.caches.add(cacheManager.getCache(BranchesCommandBuilder.CACHE_NAME));
} }
//~--- methods ------------------------------------------------------------
/** /**
* Clear caches on explicit repository cache clear event. * Clear caches on explicit repository cache clear event.
* *
@@ -347,35 +381,4 @@ public final class RepositoryServiceFactory {
} }
} }
//~--- fields ---------------------------------------------------------------
/**
* cache manager
*/
private final CacheManager cacheManager;
/**
* scm-manager configuration
*/
private final ScmConfiguration configuration;
/**
* pre processor util
*/
private final PreProcessorUtil preProcessorUtil;
/**
* repository manager
*/
private final RepositoryManager repositoryManager;
/**
* service resolvers
*/
private final Set<RepositoryServiceResolver> resolvers;
private Set<ScmProtocolProvider> protocolProviders;
private final WorkdirProvider workdirProvider;
} }

View File

@@ -27,22 +27,29 @@ package sonia.scm.repository.util;
import org.apache.shiro.SecurityUtils; import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.Subject;
import sonia.scm.repository.Person; import sonia.scm.repository.Person;
import sonia.scm.user.EMail;
import sonia.scm.user.User; import sonia.scm.user.User;
import javax.annotation.Nullable;
public class AuthorUtil { public class AuthorUtil {
public static void setAuthorIfNotAvailable(CommandWithAuthor request) { public static void setAuthorIfNotAvailable(CommandWithAuthor request) {
setAuthorIfNotAvailable(request, null);
}
public static void setAuthorIfNotAvailable(CommandWithAuthor request, @Nullable EMail eMail) {
if (request.getAuthor() == null) { if (request.getAuthor() == null) {
request.setAuthor(createAuthorFromSubject()); request.setAuthor(createAuthorFromSubject(eMail));
} }
} }
private static Person createAuthorFromSubject() { private static Person createAuthorFromSubject(@Nullable EMail eMail) {
Subject subject = SecurityUtils.getSubject(); Subject subject = SecurityUtils.getSubject();
User user = subject.getPrincipals().oneByType(User.class); User user = subject.getPrincipals().oneByType(User.class);
String name = user.getDisplayName(); String name = user.getDisplayName();
String email = user.getMail(); String mailAddress = eMail != null ? eMail.getMailOrFallback(user) : user.getMail();
return new Person(name, email); return new Person(name, mailAddress);
} }
public interface CommandWithAuthor { public interface CommandWithAuthor {

View File

@@ -0,0 +1,71 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.user;
import com.google.common.base.Strings;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.util.ValidationUtil;
import javax.inject.Inject;
/**
* Email is able to resolve email addresses of users.
*
* @since 2.8.0
*/
public class EMail {
private final ScmConfiguration scmConfiguration;
@Inject
public EMail(ScmConfiguration scmConfiguration) {
this.scmConfiguration = scmConfiguration;
}
/**
* Returns the email address of the given user or a generated fallback address.
* @param user user to resolve address from
* @return email address or fallback
*/
public String getMailOrFallback(User user) {
if (Strings.isNullOrEmpty(user.getMail())) {
if (isMailUsedAsId(user)) {
return user.getId();
} else {
return createFallbackMail(user);
}
} else {
return user.getMail();
}
}
private boolean isMailUsedAsId(User user) {
return ValidationUtil.isMailAddressValid(user.getId());
}
private String createFallbackMail(User user) {
return user.getId() + "@" + scmConfiguration.getMailDomainName();
}
}

View File

@@ -25,7 +25,12 @@
package sonia.scm.repository.api; package sonia.scm.repository.api;
import com.google.common.io.ByteSource; import com.google.common.io.ByteSource;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.api.io.TempDir;
@@ -34,10 +39,12 @@ import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock; import org.mockito.invocation.InvocationOnMock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.stubbing.Answer; import org.mockito.stubbing.Answer;
import sonia.scm.repository.Person; import sonia.scm.config.ScmConfiguration;
import sonia.scm.repository.spi.ModifyCommand; import sonia.scm.repository.spi.ModifyCommand;
import sonia.scm.repository.spi.ModifyCommandRequest; import sonia.scm.repository.spi.ModifyCommandRequest;
import sonia.scm.repository.work.WorkdirProvider; import sonia.scm.repository.work.WorkdirProvider;
import sonia.scm.user.EMail;
import sonia.scm.user.User;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
@@ -50,15 +57,19 @@ import java.util.List;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
class ModifyCommandBuilderTest { class ModifyCommandBuilderTest {
private static final ScmConfiguration SCM_CONFIGURATION = new ScmConfiguration();
@Mock @Mock
ModifyCommand command; ModifyCommand command;
@Mock @Mock
@@ -73,7 +84,7 @@ class ModifyCommandBuilderTest {
void initWorkdir(@TempDir Path temp) throws IOException { void initWorkdir(@TempDir Path temp) throws IOException {
workdir = Files.createDirectory(temp.resolve("workdir")); workdir = Files.createDirectory(temp.resolve("workdir"));
lenient().when(workdirProvider.createNewWorkdir()).thenReturn(workdir.toFile()); lenient().when(workdirProvider.createNewWorkdir()).thenReturn(workdir.toFile());
commandBuilder = new ModifyCommandBuilder(command, workdirProvider); commandBuilder = new ModifyCommandBuilder(command, workdirProvider, new EMail(SCM_CONFIGURATION));
} }
@BeforeEach @BeforeEach
@@ -89,6 +100,48 @@ class ModifyCommandBuilderTest {
); );
} }
private ModifyCommandBuilder initCommand() {
return commandBuilder
.setBranch("branch")
.setCommitMessage("message");
}
private void mockLoggedInUser(User loggedInUser) {
Subject subject = mock(Subject.class);
ThreadContext.bind(subject);
PrincipalCollection principals = mock(PrincipalCollection.class);
when(subject.getPrincipals()).thenReturn(principals);
when(principals.oneByType(User.class)).thenReturn(loggedInUser);
}
@AfterEach
void unbindSubjec() {
ThreadContext.unbindSubject();
}
private static class ExtractContent implements Answer {
private final List<String> contentCaptor;
public ExtractContent(List<String> contentCaptor) {
this.contentCaptor = contentCaptor;
}
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
return contentCaptor.add(Files.readAllLines(((File) invocation.getArgument(1)).toPath()).get(0));
}
}
@Nested
class WithUserWithMail {
@BeforeEach
void initSubject() {
User loggedInUser = new User("dent", "Arthur", "dent@hitchhiker.com");
mockLoggedInUser(loggedInUser);
}
@Test @Test
void shouldReturnTargetRevisionFromCommit() { void shouldReturnTargetRevisionFromCommit() {
String targetRevision = initCommand() String targetRevision = initCommand()
@@ -198,13 +251,6 @@ class ModifyCommandBuilderTest {
assertThat(contentCaptor).contains("content"); assertThat(contentCaptor).contains("content");
} }
private ModifyCommandBuilder initCommand() {
return commandBuilder
.setBranch("branch")
.setCommitMessage("message")
.setAuthor(new Person());
}
@Test @Test
void shouldDeleteTemporaryFiles(@TempDir Path temp) throws IOException { void shouldDeleteTemporaryFiles(@TempDir Path temp) throws IOException {
ArgumentCaptor<String> nameCaptor = ArgumentCaptor.forClass(String.class); ArgumentCaptor<String> nameCaptor = ArgumentCaptor.forClass(String.class);
@@ -218,16 +264,39 @@ class ModifyCommandBuilderTest {
assertThat(Files.list(temp)).isEmpty(); assertThat(Files.list(temp)).isEmpty();
} }
private static class ExtractContent implements Answer { @Test
private final List<String> contentCaptor; void shouldUseMailFromUser() throws IOException {
initCommand()
.modifyFile("toBeModified").withData(ByteSource.wrap("content".getBytes()))
.execute();
public ExtractContent(List<String> contentCaptor) { verify(command).execute(argThat(modifyCommandRequest -> {
this.contentCaptor = contentCaptor; assertThat(modifyCommandRequest.getAuthor().getMail()).isEqualTo("dent@hitchhiker.com");
return true;
}));
}
} }
@Override @Nested
public Object answer(InvocationOnMock invocation) throws Throwable { class WithUserWithoutMail {
return contentCaptor.add(Files.readAllLines(((File) invocation.getArgument(1)).toPath()).get(0));
@BeforeEach
void initSubject() {
User loggedInUser = new User("dent", "Arthur", null);
mockLoggedInUser(loggedInUser);
}
@Test
void shouldUseMailFromUser() throws IOException {
SCM_CONFIGURATION.setMailDomainName("heart-of-gold.local");
initCommand()
.modifyFile("toBeModified").withData(ByteSource.wrap("content".getBytes()))
.execute();
verify(command).execute(argThat(modifyCommandRequest -> {
assertThat(modifyCommandRequest.getAuthor().getMail()).isEqualTo("dent@heart-of-gold.local");
return true;
}));
} }
} }
} }

View File

@@ -46,6 +46,7 @@ import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.spi.RepositoryServiceProvider; import sonia.scm.repository.spi.RepositoryServiceProvider;
import sonia.scm.repository.spi.RepositoryServiceResolver; import sonia.scm.repository.spi.RepositoryServiceResolver;
import sonia.scm.repository.work.WorkdirProvider; import sonia.scm.repository.work.WorkdirProvider;
import sonia.scm.user.EMail;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -56,9 +57,6 @@ import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
class RepositoryServiceFactoryTest { class RepositoryServiceFactoryTest {
@Mock
private ScmConfiguration configuration;
@Mock(answer = Answers.RETURNS_MOCKS) @Mock(answer = Answers.RETURNS_MOCKS)
private CacheManager cacheManager; private CacheManager cacheManager;
@@ -94,8 +92,9 @@ class RepositoryServiceFactoryTest {
builder.add(repositoryServiceResolver); builder.add(repositoryServiceResolver);
} }
return new RepositoryServiceFactory( return new RepositoryServiceFactory(
configuration, cacheManager, repositoryManager, builder.build(), cacheManager, repositoryManager, builder.build(),
preProcessorUtil, ImmutableSet.of(), workdirProvider, eventBus preProcessorUtil, ImmutableSet.of(), workdirProvider,
new EMail(new ScmConfiguration()), eventBus
); );
} }

View File

@@ -25,9 +25,11 @@
package sonia.scm.repository.api; package sonia.scm.repository.api;
import org.junit.Test; import org.junit.Test;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.spi.HttpScmProtocol; import sonia.scm.repository.spi.HttpScmProtocol;
import sonia.scm.repository.spi.RepositoryServiceProvider; import sonia.scm.repository.spi.RepositoryServiceProvider;
import sonia.scm.user.EMail;
import javax.servlet.ServletConfig; import javax.servlet.ServletConfig;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@@ -46,9 +48,11 @@ public class RepositoryServiceTest {
private final RepositoryServiceProvider provider = mock(RepositoryServiceProvider.class); private final RepositoryServiceProvider provider = mock(RepositoryServiceProvider.class);
private final Repository repository = new Repository("", "git", "space", "repo"); private final Repository repository = new Repository("", "git", "space", "repo");
private final EMail eMail = new EMail(new ScmConfiguration());
@Test @Test
public void shouldReturnMatchingProtocolsFromProvider() { public void shouldReturnMatchingProtocolsFromProvider() {
RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null); RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null, eMail);
Stream<ScmProtocol> supportedProtocols = repositoryService.getSupportedProtocols(); Stream<ScmProtocol> supportedProtocols = repositoryService.getSupportedProtocols();
assertThat(sizeOf(supportedProtocols.collect(Collectors.toList()))).isEqualTo(1); assertThat(sizeOf(supportedProtocols.collect(Collectors.toList()))).isEqualTo(1);
@@ -56,7 +60,7 @@ public class RepositoryServiceTest {
@Test @Test
public void shouldFindKnownProtocol() { public void shouldFindKnownProtocol() {
RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null); RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null, eMail);
HttpScmProtocol protocol = repositoryService.getProtocol(HttpScmProtocol.class); HttpScmProtocol protocol = repositoryService.getProtocol(HttpScmProtocol.class);
@@ -65,11 +69,9 @@ public class RepositoryServiceTest {
@Test @Test
public void shouldFailForUnknownProtocol() { public void shouldFailForUnknownProtocol() {
RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null); RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null, eMail);
assertThrows(IllegalArgumentException.class, () -> { assertThrows(IllegalArgumentException.class, () -> repositoryService.getProtocol(UnknownScmProtocol.class));
repositoryService.getProtocol(UnknownScmProtocol.class);
});
} }
private static class DummyHttpProtocol extends HttpScmProtocol { private static class DummyHttpProtocol extends HttpScmProtocol {

View File

@@ -0,0 +1,114 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.util;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Answers;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.repository.Person;
import sonia.scm.user.EMail;
import sonia.scm.user.User;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class AuthorUtilTest {
@Mock
private EMail eMail;
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private Subject subject;
@BeforeEach
void setUpSubject() {
ThreadContext.bind(subject);
}
@AfterEach
void tearDownSubject() {
ThreadContext.unbindSubject();
}
@Test
void shouldCreateMailAddressFromEmail() {
User trillian = new User("trillian");
when(subject.getPrincipals().oneByType(User.class)).thenReturn(trillian);
when(eMail.getMailOrFallback(trillian)).thenReturn("tricia@hitchhicker.com");
Command command = new Command(null);
AuthorUtil.setAuthorIfNotAvailable(command, eMail);
assertThat(command.getAuthor().getMail()).isEqualTo("tricia@hitchhicker.com");
}
@Test
void shouldUseUsersMailAddressWithoutEMail() {
User trillian = new User("trillian", "Trillian", "trillian.mcmillan@hitchhiker.com");
when(subject.getPrincipals().oneByType(User.class)).thenReturn(trillian);
Command command = new Command(null);
AuthorUtil.setAuthorIfNotAvailable(command);
assertThat(command.getAuthor().getMail()).isEqualTo("trillian.mcmillan@hitchhiker.com");
}
@Test
void shouldKeepExistingAuthor() {
Person person = new Person("Trillian McMillan", "trillian.mcmillian@hitchhiker.com");
Command command = new Command(person);
AuthorUtil.setAuthorIfNotAvailable(command);
assertThat(command.getAuthor()).isSameAs(person);
}
public static class Command implements AuthorUtil.CommandWithAuthor {
private Person person;
public Command(Person person) {
this.person = person;
}
@Override
public Person getAuthor() {
return person;
}
@Override
public void setAuthor(Person person) {
this.person = person;
}
}
}

View File

@@ -0,0 +1,62 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.user;
import org.junit.jupiter.api.Test;
import sonia.scm.config.ScmConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
class EMailTest {
EMail eMail = new EMail(new ScmConfiguration());
@Test
void shouldUserUsersAddressIfAvailable() {
User user = new User("dent", "Arthur Dent", "arthur@hitchhiker.com");
String mailAddress = eMail.getMailOrFallback(user);
assertThat(mailAddress).isEqualTo("arthur@hitchhiker.com");
}
@Test
void shouldCreateAddressIfNoneAvailable() {
User user = new User("dent", "Arthur Dent", "");
String mailAddress = eMail.getMailOrFallback(user);
assertThat(mailAddress).isEqualTo("dent@scm-manager.local");
}
@Test
void shouldUserUsersIdIfItLooksLikeAnMailAddress() {
User user = new User("dent@hitchhiker.com", "Arthur Dent", "");
String mailAddress = eMail.getMailOrFallback(user);
assertThat(mailAddress).isEqualTo("dent@hitchhiker.com");
}
}

View File

@@ -0,0 +1,60 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC } from "react";
import { useTranslation } from "react-i18next";
import Notification from "../Notification";
import { Me } from "@scm-manager/ui-types";
import { connect } from "react-redux";
type Props = {
// props from global state
me: Me;
};
const CommitAuthor: FC<Props> = ({ me }) => {
const [t] = useTranslation("repos");
const mail = me.mail ? me.mail : me.fallbackMail;
return (
<>
{!me.mail && <Notification type="warning">{t("commit.commitAuthor.noMail")}</Notification>}
<span className="mb-2">
<strong>{t("commit.commitAuthor.author")}</strong> {`${me.displayName} <${mail}>`}
</span>
</>
);
};
const mapStateToProps = (state: any) => {
const { auth } = state;
const me = auth.me;
return {
me
};
};
export default connect(mapStateToProps)(CommitAuthor);

View File

@@ -52,6 +52,7 @@ export { default as RepositoryAvatar } from "./RepositoryAvatar";
export { default as RepositoryEntry } from "./RepositoryEntry"; export { default as RepositoryEntry } from "./RepositoryEntry";
export { default as RepositoryEntryLink } from "./RepositoryEntryLink"; export { default as RepositoryEntryLink } from "./RepositoryEntryLink";
export { default as JumpToFileButton } from "./JumpToFileButton"; export { default as JumpToFileButton } from "./JumpToFileButton";
export { default as CommitAuthor } from "./CommitAuthor";
export { export {
File, File,

View File

@@ -49,5 +49,6 @@ export type Config = {
namespaceStrategy: string; namespaceStrategy: string;
loginInfoUrl: string; loginInfoUrl: string;
releaseFeedUrl: string; releaseFeedUrl: string;
mailDomainName: string;
_links: Links; _links: Links;
}; };

View File

@@ -27,7 +27,8 @@ import { Links } from "./hal";
export type Me = { export type Me = {
name: string; name: string;
displayName: string; displayName: string;
mail: string; mail?: string;
fallbackMail?: string;
groups: string[]; groups: string[];
_links: Links; _links: Links;
}; };

View File

@@ -27,13 +27,13 @@ import { Links } from "./hal";
export type DisplayedUser = { export type DisplayedUser = {
id: string; id: string;
displayName: string; displayName: string;
mail: string; mail?: string;
}; };
export type User = { export type User = {
displayName: string; displayName: string;
name: string; name: string;
mail: string; mail?: string;
password: string; password: string;
active: boolean; active: boolean;
type?: string; type?: string;

View File

@@ -47,6 +47,7 @@
"skip-failed-authenticators": "Fehlgeschlagene Authentifizierer überspringen", "skip-failed-authenticators": "Fehlgeschlagene Authentifizierer überspringen",
"plugin-url": "Plugin Center URL", "plugin-url": "Plugin Center URL",
"release-feed-url": "Release Feed URL", "release-feed-url": "Release Feed URL",
"mail-domain-name": "Fallback E-Mail Domain Name",
"enabled-xsrf-protection": "XSRF Protection aktivieren", "enabled-xsrf-protection": "XSRF Protection aktivieren",
"enabled-user-converter": "Benutzer Konverter aktivieren", "enabled-user-converter": "Benutzer Konverter aktivieren",
"namespace-strategy": "Namespace Strategie", "namespace-strategy": "Namespace Strategie",
@@ -63,6 +64,7 @@
"dateFormatHelpText": "Moments Datumsformat. Zulässige Formate sind in der MomentJS Dokumentation beschrieben.", "dateFormatHelpText": "Moments Datumsformat. Zulässige Formate sind in der MomentJS Dokumentation beschrieben.",
"pluginUrlHelpText": "Die URL der Plugin Center API. Beschreibung der Platzhalter: version = SCM-Manager Version; os = Betriebssystem; arch = Architektur", "pluginUrlHelpText": "Die URL der Plugin Center API. Beschreibung der Platzhalter: version = SCM-Manager Version; os = Betriebssystem; arch = Architektur",
"releaseFeedUrlHelpText": "Die URL des RSS Release Feed des SCM-Manager. Darüber wird über die neue SCM-Manager Version informiert. Um diese Funktion zu deaktivieren lassen Sie dieses Feld leer.", "releaseFeedUrlHelpText": "Die URL des RSS Release Feed des SCM-Manager. Darüber wird über die neue SCM-Manager Version informiert. Um diese Funktion zu deaktivieren lassen Sie dieses Feld leer.",
"mailDomainNameHelpText": "Dieser Domain Name wird genutzt, wenn für einen User eine E-Mail-Adresse benötigt wird, für den keine hinterlegt ist. Diese Domain wird nicht zum Versenden von E-Mails genutzt und auch keine anderweitige Verbindung aufgebaut.",
"enableForwardingHelpText": "mod_proxy Port Weiterleitung aktivieren.", "enableForwardingHelpText": "mod_proxy Port Weiterleitung aktivieren.",
"disableGroupingGridHelpText": "Repository Gruppen deaktivieren. Nach einer Änderung an dieser Einstellung muss die Seite komplett neu geladen werden.", "disableGroupingGridHelpText": "Repository Gruppen deaktivieren. Nach einer Änderung an dieser Einstellung muss die Seite komplett neu geladen werden.",
"allowAnonymousAccessHelpText": "Anonyme Benutzer haben Zugriff auf freigegebene Repositories.", "allowAnonymousAccessHelpText": "Anonyme Benutzer haben Zugriff auf freigegebene Repositories.",

View File

@@ -135,6 +135,12 @@
"sources": "Sources" "sources": "Sources"
} }
}, },
"commit": {
"commitAuthor": {
"author": "Autor",
"noMail": "Für den aktuellen Benutzer existiert keine E-Mail-Adresse. Es wird die unten angezeigte generierte Adresse genutzt."
}
},
"repositoryForm": { "repositoryForm": {
"subtitle": "Repository bearbeiten", "subtitle": "Repository bearbeiten",
"submit": "Speichern", "submit": "Speichern",
@@ -269,4 +275,4 @@
"clickHere": "Klicken Sie hier um Ihre Datei hochzuladen.", "clickHere": "Klicken Sie hier um Ihre Datei hochzuladen.",
"dragAndDrop": "Sie können Ihre Datei auch direkt in die Dropzone ziehen." "dragAndDrop": "Sie können Ihre Datei auch direkt in die Dropzone ziehen."
} }
} },

View File

@@ -47,6 +47,7 @@
"skip-failed-authenticators": "Skip Failed Authenticators", "skip-failed-authenticators": "Skip Failed Authenticators",
"plugin-url": "Plugin Center URL", "plugin-url": "Plugin Center URL",
"release-feed-url": "Release Feed URL", "release-feed-url": "Release Feed URL",
"mail-domain-name": "Fallback Mail Domain Name",
"enabled-xsrf-protection": "Enabled XSRF Protection", "enabled-xsrf-protection": "Enabled XSRF Protection",
"enabled-user-converter": "Enabled User Converter", "enabled-user-converter": "Enabled User Converter",
"namespace-strategy": "Namespace Strategy", "namespace-strategy": "Namespace Strategy",
@@ -63,6 +64,7 @@
"dateFormatHelpText": "Moments date format. Please have a look at the MomentJS documentation.", "dateFormatHelpText": "Moments date format. Please have a look at the MomentJS documentation.",
"pluginUrlHelpText": "The url of the Plugin Center API. Explanation of the placeholders: version = SCM-Manager Version; os = Operation System; arch = Architecture", "pluginUrlHelpText": "The url of the Plugin Center API. Explanation of the placeholders: version = SCM-Manager Version; os = Operation System; arch = Architecture",
"releaseFeedUrlHelpText": "The url of the RSS Release Feed for SCM-Manager. This provides up-to-date version information. To disable this feature just leave the url blank.", "releaseFeedUrlHelpText": "The url of the RSS Release Feed for SCM-Manager. This provides up-to-date version information. To disable this feature just leave the url blank.",
"mailDomainNameHelpText": "This domain name will be used to create email addresses for users without one when needed. It will not be used to send mails nor will be accessed otherwise.",
"enableForwardingHelpText": "Enable mod_proxy port forwarding.", "enableForwardingHelpText": "Enable mod_proxy port forwarding.",
"disableGroupingGridHelpText": "Disable repository Groups. A complete page reload is required after a change of this value.", "disableGroupingGridHelpText": "Disable repository Groups. A complete page reload is required after a change of this value.",
"allowAnonymousAccessHelpText": "Anonymous users have access on granted repositories.", "allowAnonymousAccessHelpText": "Anonymous users have access on granted repositories.",

View File

@@ -135,6 +135,12 @@
"count_plural": "{{count}} Contributors" "count_plural": "{{count}} Contributors"
} }
}, },
"commit": {
"commitAuthor": {
"author": "Author",
"noMail": "We have found no email address for your current user. We will use the generated address shown below."
}
},
"repositoryForm": { "repositoryForm": {
"subtitle": "Edit Repository", "subtitle": "Edit Repository",
"submit": "Save", "submit": "Save",

View File

@@ -145,6 +145,7 @@ class ConfigForm extends React.Component<Props, State> {
skipFailedAuthenticators={config.skipFailedAuthenticators} skipFailedAuthenticators={config.skipFailedAuthenticators}
pluginUrl={config.pluginUrl} pluginUrl={config.pluginUrl}
releaseFeedUrl={config.releaseFeedUrl} releaseFeedUrl={config.releaseFeedUrl}
mailDomainName={config.mailDomainName}
enabledXsrfProtection={config.enabledXsrfProtection} enabledXsrfProtection={config.enabledXsrfProtection}
enabledUserConverter={config.enabledUserConverter} enabledUserConverter={config.enabledUserConverter}
namespaceStrategy={config.namespaceStrategy} namespaceStrategy={config.namespaceStrategy}

View File

@@ -36,6 +36,7 @@ type Props = WithTranslation & {
skipFailedAuthenticators: boolean; skipFailedAuthenticators: boolean;
pluginUrl: string; pluginUrl: string;
releaseFeedUrl: string; releaseFeedUrl: string;
mailDomainName: string;
enabledXsrfProtection: boolean; enabledXsrfProtection: boolean;
enabledUserConverter: boolean; enabledUserConverter: boolean;
namespaceStrategy: string; namespaceStrategy: string;
@@ -52,6 +53,7 @@ class GeneralSettings extends React.Component<Props> {
loginInfoUrl, loginInfoUrl,
pluginUrl, pluginUrl,
releaseFeedUrl, releaseFeedUrl,
mailDomainName,
enabledXsrfProtection, enabledXsrfProtection,
enabledUserConverter, enabledUserConverter,
anonymousMode, anonymousMode,
@@ -140,6 +142,15 @@ class GeneralSettings extends React.Component<Props> {
helpText={t("help.releaseFeedUrlHelpText")} helpText={t("help.releaseFeedUrlHelpText")}
/> />
</div> </div>
<div className="column is-half">
<InputField
label={t("general-settings.mail-domain-name")}
onChange={this.handleMailDomainNameChange}
value={mailDomainName}
disabled={!hasUpdatePermission}
helpText={t("help.mailDomainNameHelpText")}
/>
</div>
<div className="column is-half"> <div className="column is-half">
<Checkbox <Checkbox
label={t("general-settings.enabled-user-converter")} label={t("general-settings.enabled-user-converter")}
@@ -179,6 +190,9 @@ class GeneralSettings extends React.Component<Props> {
handleReleaseFeedUrlChange = (value: string) => { handleReleaseFeedUrlChange = (value: string) => {
this.props.onChange(true, value, "releaseFeedUrl"); this.props.onChange(true, value, "releaseFeedUrl");
}; };
handleMailDomainNameChange = (value: string) => {
this.props.onChange(true, value, "mailDomainName");
};
} }
export default withTranslation("config")(GeneralSettings); export default withTranslation("config")(GeneralSettings);

View File

@@ -264,7 +264,7 @@ class UserForm extends React.Component<Props, State> {
handleEmailChange = (mail: string) => { handleEmailChange = (mail: string) => {
this.setState({ this.setState({
mailValidationError: !validator.isMailValid(mail), mailValidationError: !!mail && !validator.isMailValid(mail),
user: { user: {
...this.state.user, ...this.state.user,
mail mail

View File

@@ -60,6 +60,7 @@ public class ConfigDto extends HalRepresentation {
private String namespaceStrategy; private String namespaceStrategy;
private String loginInfoUrl; private String loginInfoUrl;
private String releaseFeedUrl; private String releaseFeedUrl;
private String mailDomainName;
@Override @Override
@SuppressWarnings("squid:S1185") // We want to have this method available in this package @SuppressWarnings("squid:S1185") // We want to have this method available in this package

View File

@@ -24,6 +24,7 @@
package sonia.scm.api.v2.resources; package sonia.scm.api.v2.resources;
import com.fasterxml.jackson.annotation.JsonInclude;
import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Embedded;
import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Links; import de.otto.edison.hal.Links;
@@ -40,7 +41,10 @@ public class MeDto extends HalRepresentation {
private String name; private String name;
private String displayName; private String displayName;
@JsonInclude(JsonInclude.Include.NON_NULL)
private String mail; private String mail;
@JsonInclude(JsonInclude.Include.NON_NULL)
private String fallbackMail;
private Set<String> groups; private Set<String> groups;
MeDto(Links links, Embedded embedded) { MeDto(Links links, Embedded embedded) {

View File

@@ -24,12 +24,14 @@
package sonia.scm.api.v2.resources; package sonia.scm.api.v2.resources;
import com.google.common.base.Strings;
import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Embedded;
import de.otto.edison.hal.Links; import de.otto.edison.hal.Links;
import org.apache.shiro.SecurityUtils; import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.Subject;
import sonia.scm.group.GroupCollector; import sonia.scm.group.GroupCollector;
import sonia.scm.user.EMail;
import sonia.scm.user.User; import sonia.scm.user.User;
import sonia.scm.user.UserManager; import sonia.scm.user.UserManager;
import sonia.scm.user.UserPermissions; import sonia.scm.user.UserPermissions;
@@ -46,12 +48,14 @@ public class MeDtoFactory extends HalAppenderMapper {
private final ResourceLinks resourceLinks; private final ResourceLinks resourceLinks;
private final UserManager userManager; private final UserManager userManager;
private final GroupCollector groupCollector; private final GroupCollector groupCollector;
private final EMail eMail;
@Inject @Inject
public MeDtoFactory(ResourceLinks resourceLinks, UserManager userManager, GroupCollector groupCollector) { public MeDtoFactory(ResourceLinks resourceLinks, UserManager userManager, GroupCollector groupCollector, EMail eMail) {
this.resourceLinks = resourceLinks; this.resourceLinks = resourceLinks;
this.userManager = userManager; this.userManager = userManager;
this.groupCollector = groupCollector; this.groupCollector = groupCollector;
this.eMail = eMail;
} }
public MeDto create() { public MeDto create() {
@@ -61,6 +65,7 @@ public class MeDtoFactory extends HalAppenderMapper {
MeDto dto = createDto(user); MeDto dto = createDto(user);
mapUserProperties(user, dto); mapUserProperties(user, dto);
mapGroups(user, dto); mapGroups(user, dto);
setGeneratedMail(user, dto);
return dto; return dto;
} }
@@ -79,6 +84,12 @@ public class MeDtoFactory extends HalAppenderMapper {
return subject.getPrincipals(); return subject.getPrincipals();
} }
private void setGeneratedMail(User user, MeDto dto) {
if (Strings.isNullOrEmpty(user.getMail())) {
dto.setFallbackMail(eMail.getMailOrFallback(user));
}
}
private MeDto createDto(User user) { private MeDto createDto(User user) {
Links.Builder linksBuilder = linkingTo().self(resourceLinks.me().self()); Links.Builder linksBuilder = linkingTo().self(resourceLinks.me().self());

View File

@@ -47,7 +47,8 @@ public class UserDto extends HalRepresentation {
private String displayName; private String displayName;
@JsonInclude(JsonInclude.Include.NON_NULL) @JsonInclude(JsonInclude.Include.NON_NULL)
private Instant lastModified; private Instant lastModified;
@NotEmpty @Email @JsonInclude(JsonInclude.Include.NON_NULL)
@Email
private String mail; private String mail;
@Pattern(regexp = ValidationUtil.REGEX_NAME) @Pattern(regexp = ValidationUtil.REGEX_NAME)
private String name; private String name;

View File

@@ -75,6 +75,7 @@ public class ConfigDtoToScmConfigurationMapperTest {
assertFalse(config.isEnabledUserConverter()); assertFalse(config.isEnabledUserConverter());
assertEquals("username", config.getNamespaceStrategy()); assertEquals("username", config.getNamespaceStrategy());
assertEquals("https://scm-manager.org/login-info", config.getLoginInfoUrl()); assertEquals("https://scm-manager.org/login-info", config.getLoginInfoUrl());
assertEquals("hitchhiker.mail", config.getMailDomainName());
} }
@Test @Test
@@ -113,6 +114,7 @@ public class ConfigDtoToScmConfigurationMapperTest {
configDto.setEnabledXsrfProtection(true); configDto.setEnabledXsrfProtection(true);
configDto.setNamespaceStrategy("username"); configDto.setNamespaceStrategy("username");
configDto.setLoginInfoUrl("https://scm-manager.org/login-info"); configDto.setLoginInfoUrl("https://scm-manager.org/login-info");
configDto.setMailDomainName("hitchhiker.mail");
configDto.setEnabledUserConverter(false); configDto.setEnabledUserConverter(false);
return configDto; return configDto;

View File

@@ -38,9 +38,9 @@ import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness; import org.mockito.quality.Strictness;
import sonia.scm.SCMContext; import sonia.scm.SCMContext;
import sonia.scm.group.GroupCollector; import sonia.scm.group.GroupCollector;
import sonia.scm.user.EMail;
import sonia.scm.user.User; import sonia.scm.user.User;
import sonia.scm.user.UserManager; import sonia.scm.user.UserManager;
import sonia.scm.user.UserPermissions;
import sonia.scm.user.UserTestData; import sonia.scm.user.UserTestData;
import java.net.URI; import java.net.URI;
@@ -65,13 +65,16 @@ class MeDtoFactoryTest {
@Mock @Mock
private Subject subject; private Subject subject;
@Mock
private EMail eMail;
private MeDtoFactory meDtoFactory; private MeDtoFactory meDtoFactory;
@BeforeEach @BeforeEach
void setUpContext() { void setUpContext() {
ThreadContext.bind(subject); ThreadContext.bind(subject);
ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
meDtoFactory = new MeDtoFactory(resourceLinks, userManager, groupCollector); meDtoFactory = new MeDtoFactory(resourceLinks, userManager, groupCollector, eMail);
} }
@AfterEach @AfterEach
@@ -235,4 +238,17 @@ class MeDtoFactoryTest {
MeDto dto = meDtoFactory.create(); MeDto dto = meDtoFactory.create();
assertThat(dto.getLinks().getLinkBy("profile").get().getHref()).isEqualTo("http://hitchhiker.com/users/trillian"); assertThat(dto.getLinks().getLinkBy("profile").get().getHref()).isEqualTo("http://hitchhiker.com/users/trillian");
} }
@Test
void shouldUserGeneratedMailOnlyWhenUserHasNone() {
User user = UserTestData.createTrillian();
user.setMail(null);
prepareSubject(user);
when(eMail.getMailOrFallback(user)).thenReturn("trillian@hitchhiker.local");
MeDto dto = meDtoFactory.create();
assertThat(dto.getMail()).isNull();
assertThat(dto.getFallbackMail()).isEqualTo("trillian@hitchhiker.local");
}
} }

View File

@@ -106,6 +106,7 @@ public class ScmConfigurationToConfigDtoMapperTest {
assertEquals("username", dto.getNamespaceStrategy()); assertEquals("username", dto.getNamespaceStrategy());
assertEquals("https://scm-manager.org/login-info", dto.getLoginInfoUrl()); assertEquals("https://scm-manager.org/login-info", dto.getLoginInfoUrl());
assertEquals("https://www.scm-manager.org/download/rss.xml", dto.getReleaseFeedUrl()); assertEquals("https://www.scm-manager.org/download/rss.xml", dto.getReleaseFeedUrl());
assertEquals("scm-manager.local", dto.getMailDomainName());
assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref()); assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref());
assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("update").get().getHref()); assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("update").get().getHref());