diff --git a/CHANGELOG.md b/CHANGELOG.md index ca5d9ede3a..4aa440222b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Added +- Generation of email addresses for users, where none is configured ([#1370](https://github.com/scm-manager/scm-manager/pull/1370)) - Source code fullscreen view ([#1376](https://github.com/scm-manager/scm-manager/pull/1376)) +### Fixed +- Missing default permission to manage public gpg keys ([#1377](https://github.com/scm-manager/scm-manager/pull/1377)) + +## [2.6.3] - 2020-10-16 +### Fixed +- Missing default permission to manage public gpg keys ([#1377](https://github.com/scm-manager/scm-manager/pull/1377)) + ## [2.7.1] - 2020-10-14 ### Fixed - Null Pointer Exception on anonymous migration with deleted repositories ([#1371](https://github.com/scm-manager/scm-manager/pull/1371)) @@ -362,3 +370,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [2.6.0]: https://www.scm-manager.org/download/2.6.0 [2.6.1]: https://www.scm-manager.org/download/2.6.1 [2.6.2]: https://www.scm-manager.org/download/2.6.2 +[2.6.3]: https://www.scm-manager.org/download/2.6.3 +[2.7.0]: https://www.scm-manager.org/download/2.7.0 +[2.7.1]: https://www.scm-manager.org/download/2.7.1 diff --git a/Jenkinsfile b/Jenkinsfile index 2ef5929e09..c6dc70b8d7 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -7,7 +7,8 @@ import com.cloudogu.ces.cesbuildlib.* node('docker') { - mainBranch = 'develop' + developmentBranch = 'develop' + mainBranch = 'master' properties([ // Keep only the last 10 build to preserve space @@ -51,9 +52,9 @@ node('docker') { sh "git config 'remote.origin.fetch' '+refs/heads/*:refs/remotes/origin/*'" sh "git fetch --all" - // merge release branch into master - sh "git checkout master" - sh "git reset --hard origin/master" + // merge release branch into main branch + sh "git checkout ${mainBranch}" + sh "git reset --hard origin/${mainBranch}" sh "git merge --ff-only ${env.BRANCH_NAME}" // set tag @@ -87,7 +88,7 @@ node('docker') { sonarQube.analyzeWith(mvn) } - if (isBuildSuccessful() && (isMainBranch() || isReleaseBranch())) { + if (isBuildSuccessful() && (isDevelopmentBranch() || isReleaseBranch())) { def commitHash = git.getCommitHash() def imageVersion = mvn.getVersion() @@ -143,20 +144,28 @@ node('docker') { } stage('Presentation Environment') { - build job: 'scm-manager/next-scm.cloudogu.com', propagate: false, wait: false, parameters: [ - string(name: 'changeset', value: commitHash), - string(name: 'imageTag', value: imageVersion) - ] + // we don't use developmentBranch, because we only want the lastest version of develop branch on + // next-scm. We don't want a support branch or something similar on the presentation environment. + if ("develop".equals(env.BRANCH_NAME)) { + build job: 'scm-manager/next-scm.cloudogu.com', propagate: false, wait: false, parameters: [ + string(name: 'changeset', value: commitHash), + string(name: 'imageTag', value: imageVersion) + ] + } } if (isReleaseBranch()) { stage('Update Repository') { // merge changes into develop - sh "git checkout develop" + sh "git checkout ${developmentBranch}" + // TODO what if we have a conflict // e.g.: someone has edited the changelog during the release - sh "git merge master" + if (!developmentBranch.equals(mainBranch)) { + sh "git merge ${mainBranch}" + } + // set versions for maven packages mvn "build-helper:parse-version versions:set -DgenerateBackupPoms=false -DnewVersion='\${parsedVersion.majorVersion}.\${parsedVersion.nextMinorVersion}.0-SNAPSHOT'" @@ -176,8 +185,10 @@ node('docker') { // push changes back to remote repository withCredentials([usernamePassword(credentialsId: 'cesmarvin-github', usernameVariable: 'GIT_AUTH_USR', passwordVariable: 'GIT_AUTH_PSW')]) { - sh "git -c credential.helper=\"!f() { echo username='\$GIT_AUTH_USR'; echo password='\$GIT_AUTH_PSW'; }; f\" push origin master --tags" - sh "git -c credential.helper=\"!f() { echo username='\$GIT_AUTH_USR'; echo password='\$GIT_AUTH_PSW'; }; f\" push origin develop --tags" + sh "git -c credential.helper=\"!f() { echo username='\$GIT_AUTH_USR'; echo password='\$GIT_AUTH_PSW'; }; f\" push origin ${mainBranch} --tags" + if (!developmentBranch.equals(mainBranch)) { + sh "git -c credential.helper=\"!f() { echo username='\$GIT_AUTH_USR'; echo password='\$GIT_AUTH_PSW'; }; f\" push origin develop --tags" + } sh "git -c credential.helper=\"!f() { echo username='\$GIT_AUTH_USR'; echo password='\$GIT_AUTH_PSW'; }; f\" push origin :${env.BRANCH_NAME}" } } @@ -189,6 +200,7 @@ node('docker') { } } +String developmentBranch String mainBranch Maven setupMavenBuild() { @@ -201,7 +213,7 @@ Maven setupMavenBuild() { mvn.additionalArgs += " -Dscm-it.logbackConfiguration=${logConf}" mvn.additionalArgs += " -Dsonar.coverage.exclusions=**/*.test.ts,**/*.test.tsx,**/*.stories.tsx" - if (isMainBranch() || isReleaseBranch()) { + if (isDevelopmentBranch() || isReleaseBranch()) { // Release starts javadoc, which takes very long, so do only for certain branches mvn.additionalArgs += ' -DperformRelease' // JDK8 is more strict, we should fix this before the next release. Right now, this is just not the focus, yet. @@ -218,8 +230,8 @@ String getReleaseVersion() { return env.BRANCH_NAME.substring("release/".length()); } -boolean isMainBranch() { - return mainBranch.equals(env.BRANCH_NAME) +boolean isDevelopmentBranch() { + return developmentBranch.equals(env.BRANCH_NAME) } void withGPGEnvironment(def closure) { diff --git a/docs/en/development/intellij-idea-configuration.md b/docs/en/development/intellij-idea-configuration.md index f740281518..d415b4d206 100644 --- a/docs/en/development/intellij-idea-configuration.md +++ b/docs/en/development/intellij-idea-configuration.md @@ -11,6 +11,12 @@ title: Intellij IDEA Configuration ### 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 * Add Maven * Name: run-backend diff --git a/pom.xml b/pom.xml index 506c11920f..c7339ecf73 100644 --- a/pom.xml +++ b/pom.xml @@ -903,7 +903,7 @@ - 3.5.11 + 3.5.13 2.1 5.7.0 @@ -913,7 +913,7 @@ 3.1.0 2.1.1 - 4.5.7.Final + 4.5.8.Final 1.19.4 2.11.2 4.2.3 diff --git a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java index 7bb64a9454..6f1414b8e0 100644 --- a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java +++ b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java @@ -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"; + /** + * 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 */ @@ -187,6 +195,8 @@ public class ScmConfiguration implements Configuration { @XmlElement(name = "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)} @@ -227,6 +237,7 @@ public class ScmConfiguration implements Configuration { this.namespaceStrategy = other.namespaceStrategy; this.loginInfoUrl = other.loginInfoUrl; this.releaseFeedUrl = other.releaseFeedUrl; + this.mailDomainName = other.mailDomainName; } /** @@ -291,6 +302,15 @@ public class ScmConfiguration implements Configuration { 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 * proxy settings. @@ -471,6 +491,16 @@ public class ScmConfiguration implements Configuration { 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 * settings. diff --git a/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java index 06a3b489a1..6cbf2caa8c 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java @@ -30,7 +30,9 @@ import sonia.scm.repository.spi.MergeCommand; import sonia.scm.repository.spi.MergeCommandRequest; import sonia.scm.repository.spi.MergeConflictResult; import sonia.scm.repository.util.AuthorUtil; +import sonia.scm.user.EMail; +import javax.annotation.Nullable; import java.util.Set; /** @@ -78,8 +80,12 @@ public class MergeCommandBuilder { private final MergeCommand mergeCommand; private final MergeCommandRequest request = new MergeCommandRequest(); - MergeCommandBuilder(MergeCommand mergeCommand) { + @Nullable + private final EMail eMail; + + MergeCommandBuilder(MergeCommand mergeCommand, @Nullable EMail eMail) { this.mergeCommand = mergeCommand; + this.eMail = eMail; } /** @@ -209,7 +215,7 @@ public class MergeCommandBuilder { * @return The result of the merge. */ public MergeCommandResult executeMerge() { - AuthorUtil.setAuthorIfNotAvailable(request); + AuthorUtil.setAuthorIfNotAvailable(request, eMail); Preconditions.checkArgument(request.isValid(), "revision to merge and target revision is required"); return mergeCommand.merge(request); } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java index aeccb24b99..f2b11a00a0 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java @@ -35,8 +35,10 @@ import sonia.scm.repository.spi.ModifyCommand; import sonia.scm.repository.spi.ModifyCommandRequest; import sonia.scm.repository.util.AuthorUtil; import sonia.scm.repository.work.WorkdirProvider; +import sonia.scm.user.EMail; import sonia.scm.util.IOUtil; +import javax.annotation.Nullable; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -51,7 +53,6 @@ import java.util.function.Consumer; * default a {@link sonia.scm.AlreadyExistsException} will be thrown) *
  • modify existing files ({@link #modifyFile(String)}
  • *
  • delete existing files ({@link #deleteFile(String)}
  • - *
  • move/rename existing files ({@link #moveFile(String, String)}
  • * * * 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 File workdir; + @Nullable + private final EMail eMail; + private final ModifyCommandRequest request = new ModifyCommandRequest(); - ModifyCommandBuilder(ModifyCommand command, WorkdirProvider workdirProvider) { + ModifyCommandBuilder(ModifyCommand command, WorkdirProvider workdirProvider, @Nullable EMail eMail) { this.command = command; this.workdir = workdirProvider.createNewWorkdir(); + this.eMail = eMail; } /** @@ -124,7 +129,7 @@ public class ModifyCommandBuilder { * @return The revision of the new commit. */ public String execute() { - AuthorUtil.setAuthorIfNotAvailable(request); + AuthorUtil.setAuthorIfNotAvailable(request, eMail); try { Preconditions.checkArgument(request.isValid(), "commit message and at least one request are required"); return command.execute(request); diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java index 15f84a4ecd..aa2a41782d 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java @@ -34,7 +34,9 @@ import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.spi.RepositoryServiceProvider; import sonia.scm.repository.work.WorkdirProvider; +import sonia.scm.user.EMail; +import javax.annotation.Nullable; import java.io.Closeable; import java.io.IOException; import java.util.Set; @@ -84,30 +86,36 @@ public final class RepositoryService implements Closeable { private final PreProcessorUtil preProcessorUtil; private final RepositoryServiceProvider provider; private final Repository repository; - @SuppressWarnings("rawtypes") + @SuppressWarnings({"rawtypes", "java:S3740"}) private final Set protocolProviders; private final WorkdirProvider workdirProvider; + @Nullable + private final EMail eMail; + /** * Constructs a new {@link RepositoryService}. This constructor should only * be called from the {@link RepositoryServiceFactory}. * @param cacheManager cache manager * @param provider implementation for {@link RepositoryServiceProvider} * @param repository the repository - * @param workdirProvider + * @param workdirProvider provider for workdirs + * @param eMail utility to compute email addresses if missing */ RepositoryService(CacheManager cacheManager, - RepositoryServiceProvider provider, Repository repository, + RepositoryServiceProvider provider, + Repository repository, PreProcessorUtil preProcessorUtil, - @SuppressWarnings("rawtypes") Set protocolProviders, - WorkdirProvider workdirProvider - ) { + @SuppressWarnings({"rawtypes", "java:S3740"}) Set protocolProviders, + WorkdirProvider workdirProvider, + @Nullable EMail eMail) { this.cacheManager = cacheManager; this.provider = provider; this.repository = repository; this.preProcessorUtil = preProcessorUtil; this.protocolProviders = protocolProviders; this.workdirProvider = workdirProvider; + this.eMail = eMail; } /** @@ -397,7 +405,7 @@ public final class RepositoryService implements Closeable { LOG.debug("create merge command for repository {}", 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 {}", 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); } - @SuppressWarnings("rawtypes") + @SuppressWarnings({"rawtypes", "java:S3740"}) private ScmProtocol createProviderInstanceForRepository(ScmProtocolProvider protocolProvider) { return protocolProvider.get(repository); } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java index b030d1cb0c..2c13cb63be 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java @@ -42,7 +42,6 @@ import sonia.scm.cache.Cache; import sonia.scm.cache.CacheManager; import sonia.scm.config.ScmConfiguration; import sonia.scm.event.ScmEventBus; -import sonia.scm.repository.BranchCreatedEvent; import sonia.scm.repository.ClearRepositoryCacheEvent; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.PostReceiveRepositoryHookEvent; @@ -58,7 +57,9 @@ import sonia.scm.repository.work.WorkdirProvider; import sonia.scm.security.PublicKeyCreatedEvent; import sonia.scm.security.PublicKeyDeletedEvent; import sonia.scm.security.ScmSecurityException; +import sonia.scm.user.EMail; +import javax.annotation.Nullable; import java.util.Set; import static sonia.scm.ContextEntry.ContextBuilder.entity; @@ -115,7 +116,17 @@ public final class RepositoryServiceFactory { private static final Logger logger = LoggerFactory.getLogger(RepositoryServiceFactory.class); - //~--- constructors --------------------------------------------------------- + private final CacheManager cacheManager; + private final RepositoryManager repositoryManager; + private final Set resolvers; + private final PreProcessorUtil preProcessorUtil; + @SuppressWarnings({"rawtypes", "java:S3740"}) + private final Set protocolProviders; + private final WorkdirProvider workdirProvider; + + @Nullable + private final EMail eMail; + /** * Constructs a new {@link RepositoryServiceFactory}. This constructor @@ -127,40 +138,67 @@ public final class RepositoryServiceFactory { * @param repositoryManager manager for repositories * @param resolvers a set of {@link RepositoryServiceResolver} * @param preProcessorUtil helper object for pre processor handling - * @param protocolProviders - * @param workdirProvider + * @param protocolProviders providers for repository protocols + * @param workdirProvider provider for working directories + * + * @deprecated use {@link RepositoryServiceFactory#RepositoryServiceFactory(CacheManager, RepositoryManager, Set, PreProcessorUtil, Set, WorkdirProvider, EMail)} instead * @since 1.21 */ - @Inject + @Deprecated public RepositoryServiceFactory(ScmConfiguration configuration, CacheManager cacheManager, RepositoryManager repositoryManager, Set resolvers, PreProcessorUtil preProcessorUtil, - @SuppressWarnings("rawtypes") Set protocolProviders, WorkdirProvider workdirProvider) { + @SuppressWarnings({"rawtypes", "java:S3740"}) Set protocolProviders, + WorkdirProvider workdirProvider) { this( - configuration, cacheManager, repositoryManager, resolvers, - preProcessorUtil, protocolProviders, workdirProvider, ScmEventBus.getInstance() + cacheManager, repositoryManager, resolvers, + 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 resolvers, PreProcessorUtil preProcessorUtil, + @SuppressWarnings({"rawtypes", "java:S3740"}) Set protocolProviders, + WorkdirProvider workdirProvider, EMail eMail) { + this( + cacheManager, repositoryManager, resolvers, + preProcessorUtil, protocolProviders, workdirProvider, + eMail, ScmEventBus.getInstance() ); } @VisibleForTesting - RepositoryServiceFactory(ScmConfiguration configuration, - CacheManager cacheManager, RepositoryManager repositoryManager, + @SuppressWarnings("java:S107") // to keep backward compatibility, we can not reduce amount of parameters + RepositoryServiceFactory(CacheManager cacheManager, RepositoryManager repositoryManager, Set resolvers, PreProcessorUtil preProcessorUtil, - Set protocolProviders, WorkdirProvider workdirProvider, - ScmEventBus eventBus) { - this.configuration = configuration; + @SuppressWarnings({"rawtypes", "java:S3740"}) Set protocolProviders, + WorkdirProvider workdirProvider, @Nullable EMail eMail, ScmEventBus eventBus) { this.cacheManager = cacheManager; this.repositoryManager = repositoryManager; this.resolvers = resolvers; this.preProcessorUtil = preProcessorUtil; this.protocolProviders = protocolProviders; this.workdirProvider = workdirProvider; + this.eMail = eMail; eventBus.register(new CacheClearHook(cacheManager)); } - //~--- methods -------------------------------------------------------------- - /** * Creates a new RepositoryService for the given repository. * @@ -246,7 +284,7 @@ public final class RepositoryServiceFactory { } service = new RepositoryService(cacheManager, provider, repository, - preProcessorUtil, protocolProviders, workdirProvider); + preProcessorUtil, protocolProviders, workdirProvider, eMail); break; } @@ -259,8 +297,6 @@ public final class RepositoryServiceFactory { return service; } - //~--- inner classes -------------------------------------------------------- - /** * 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)); } - //~--- methods ------------------------------------------------------------ - /** * 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 resolvers; - - private Set protocolProviders; - - private final WorkdirProvider workdirProvider; } diff --git a/scm-core/src/main/java/sonia/scm/repository/util/AuthorUtil.java b/scm-core/src/main/java/sonia/scm/repository/util/AuthorUtil.java index fa2c598f03..e5a6501604 100644 --- a/scm-core/src/main/java/sonia/scm/repository/util/AuthorUtil.java +++ b/scm-core/src/main/java/sonia/scm/repository/util/AuthorUtil.java @@ -21,28 +21,35 @@ * 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.SecurityUtils; import org.apache.shiro.subject.Subject; import sonia.scm.repository.Person; +import sonia.scm.user.EMail; import sonia.scm.user.User; +import javax.annotation.Nullable; + public class AuthorUtil { public static void setAuthorIfNotAvailable(CommandWithAuthor request) { + setAuthorIfNotAvailable(request, null); + } + + public static void setAuthorIfNotAvailable(CommandWithAuthor request, @Nullable EMail eMail) { 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(); User user = subject.getPrincipals().oneByType(User.class); String name = user.getDisplayName(); - String email = user.getMail(); - return new Person(name, email); + String mailAddress = eMail != null ? eMail.getMailOrFallback(user) : user.getMail(); + return new Person(name, mailAddress); } public interface CommandWithAuthor { diff --git a/scm-core/src/main/java/sonia/scm/user/EMail.java b/scm-core/src/main/java/sonia/scm/user/EMail.java new file mode 100644 index 0000000000..e525b8d46c --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/user/EMail.java @@ -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(); + } +} diff --git a/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java b/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java index 5583261e52..281e793cbb 100644 --- a/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java +++ b/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java @@ -25,7 +25,12 @@ package sonia.scm.repository.api; 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.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.io.TempDir; @@ -34,10 +39,12 @@ import org.mockito.Mock; import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.jupiter.MockitoExtension; 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.ModifyCommandRequest; import sonia.scm.repository.work.WorkdirProvider; +import sonia.scm.user.EMail; +import sonia.scm.user.User; import java.io.ByteArrayInputStream; import java.io.File; @@ -50,15 +57,19 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class ModifyCommandBuilderTest { + private static final ScmConfiguration SCM_CONFIGURATION = new ScmConfiguration(); + @Mock ModifyCommand command; @Mock @@ -73,7 +84,7 @@ class ModifyCommandBuilderTest { void initWorkdir(@TempDir Path temp) throws IOException { workdir = Files.createDirectory(temp.resolve("workdir")); lenient().when(workdirProvider.createNewWorkdir()).thenReturn(workdir.toFile()); - commandBuilder = new ModifyCommandBuilder(command, workdirProvider); + commandBuilder = new ModifyCommandBuilder(command, workdirProvider, new EMail(SCM_CONFIGURATION)); } @BeforeEach @@ -89,136 +100,27 @@ class ModifyCommandBuilderTest { ); } - @Test - void shouldReturnTargetRevisionFromCommit() { - String targetRevision = initCommand() - .deleteFile("toBeDeleted") - .execute(); - - assertThat(targetRevision).isEqualTo("target"); - } - - @Test - void shouldExecuteDelete() throws IOException { - initCommand() - .deleteFile("toBeDeleted") - .execute(); - - verify(worker).delete("toBeDeleted"); - } - - @Test - void shouldExecuteCreateWithByteSourceContent() throws IOException { - ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); - List contentCaptor = new ArrayList<>(); - doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any(), anyBoolean()); - - initCommand() - .createFile("toBeCreated").withData(ByteSource.wrap("content".getBytes())) - .execute(); - - assertThat(nameCaptor.getValue()).isEqualTo("toBeCreated"); - assertThat(contentCaptor).contains("content"); - } - - @Test - void shouldExecuteCreateWithInputStreamContent() throws IOException { - ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); - List contentCaptor = new ArrayList<>(); - doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any(), anyBoolean()); - - initCommand() - .createFile("toBeCreated").withData(new ByteArrayInputStream("content".getBytes())) - .execute(); - - assertThat(nameCaptor.getValue()).isEqualTo("toBeCreated"); - assertThat(contentCaptor).contains("content"); - } - - @Test - void shouldExecuteCreateWithOverwriteFalseAsDefault() throws IOException { - ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor overwriteCaptor = ArgumentCaptor.forClass(Boolean.class); - List contentCaptor = new ArrayList<>(); - doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any(), overwriteCaptor.capture()); - - initCommand() - .createFile("toBeCreated").withData(new ByteArrayInputStream("content".getBytes())) - .execute(); - - assertThat(nameCaptor.getValue()).isEqualTo("toBeCreated"); - assertThat(overwriteCaptor.getValue()).isFalse(); - assertThat(contentCaptor).contains("content"); - } - - @Test - void shouldExecuteCreateWithOverwriteIfSet() throws IOException { - ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor overwriteCaptor = ArgumentCaptor.forClass(Boolean.class); - List contentCaptor = new ArrayList<>(); - doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any(), overwriteCaptor.capture()); - - initCommand() - .createFile("toBeCreated").setOverwrite(true).withData(new ByteArrayInputStream("content".getBytes())) - .execute(); - - assertThat(nameCaptor.getValue()).isEqualTo("toBeCreated"); - assertThat(overwriteCaptor.getValue()).isTrue(); - assertThat(contentCaptor).contains("content"); - } - - @Test - void shouldExecuteCreateMultipleTimes() throws IOException { - ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); - List contentCaptor = new ArrayList<>(); - doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any(), anyBoolean()); - - initCommand() - .createFile("toBeCreated_1").withData(new ByteArrayInputStream("content_1".getBytes())) - .createFile("toBeCreated_2").withData(new ByteArrayInputStream("content_2".getBytes())) - .execute(); - - List createdNames = nameCaptor.getAllValues(); - assertThat(createdNames.get(0)).isEqualTo("toBeCreated_1"); - assertThat(createdNames.get(1)).isEqualTo("toBeCreated_2"); - assertThat(contentCaptor).contains("content_1", "content_2"); - } - - @Test - void shouldExecuteModify() throws IOException { - ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); - List contentCaptor = new ArrayList<>(); - doAnswer(new ExtractContent(contentCaptor)).when(worker).modify(nameCaptor.capture(), any()); - - initCommand() - .modifyFile("toBeModified").withData(ByteSource.wrap("content".getBytes())) - .execute(); - - assertThat(nameCaptor.getValue()).isEqualTo("toBeModified"); - assertThat(contentCaptor).contains("content"); - } - private ModifyCommandBuilder initCommand() { return commandBuilder .setBranch("branch") - .setCommitMessage("message") - .setAuthor(new Person()); + .setCommitMessage("message"); } - @Test - void shouldDeleteTemporaryFiles(@TempDir Path temp) throws IOException { - ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor fileCaptor = ArgumentCaptor.forClass(File.class); - doNothing().when(worker).modify(nameCaptor.capture(), fileCaptor.capture()); + 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); + } - initCommand() - .modifyFile("toBeModified").withData(ByteSource.wrap("content".getBytes())) - .execute(); - - assertThat(Files.list(temp)).isEmpty(); + @AfterEach + void unbindSubjec() { + ThreadContext.unbindSubject(); } private static class ExtractContent implements Answer { + private final List contentCaptor; public ExtractContent(List contentCaptor) { @@ -230,4 +132,171 @@ class ModifyCommandBuilderTest { 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 + void shouldReturnTargetRevisionFromCommit() { + String targetRevision = initCommand() + .deleteFile("toBeDeleted") + .execute(); + + assertThat(targetRevision).isEqualTo("target"); + } + + @Test + void shouldExecuteDelete() throws IOException { + initCommand() + .deleteFile("toBeDeleted") + .execute(); + + verify(worker).delete("toBeDeleted"); + } + + @Test + void shouldExecuteCreateWithByteSourceContent() throws IOException { + ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); + List contentCaptor = new ArrayList<>(); + doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any(), anyBoolean()); + + initCommand() + .createFile("toBeCreated").withData(ByteSource.wrap("content".getBytes())) + .execute(); + + assertThat(nameCaptor.getValue()).isEqualTo("toBeCreated"); + assertThat(contentCaptor).contains("content"); + } + + @Test + void shouldExecuteCreateWithInputStreamContent() throws IOException { + ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); + List contentCaptor = new ArrayList<>(); + doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any(), anyBoolean()); + + initCommand() + .createFile("toBeCreated").withData(new ByteArrayInputStream("content".getBytes())) + .execute(); + + assertThat(nameCaptor.getValue()).isEqualTo("toBeCreated"); + assertThat(contentCaptor).contains("content"); + } + + @Test + void shouldExecuteCreateWithOverwriteFalseAsDefault() throws IOException { + ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor overwriteCaptor = ArgumentCaptor.forClass(Boolean.class); + List contentCaptor = new ArrayList<>(); + doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any(), overwriteCaptor.capture()); + + initCommand() + .createFile("toBeCreated").withData(new ByteArrayInputStream("content".getBytes())) + .execute(); + + assertThat(nameCaptor.getValue()).isEqualTo("toBeCreated"); + assertThat(overwriteCaptor.getValue()).isFalse(); + assertThat(contentCaptor).contains("content"); + } + + @Test + void shouldExecuteCreateWithOverwriteIfSet() throws IOException { + ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor overwriteCaptor = ArgumentCaptor.forClass(Boolean.class); + List contentCaptor = new ArrayList<>(); + doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any(), overwriteCaptor.capture()); + + initCommand() + .createFile("toBeCreated").setOverwrite(true).withData(new ByteArrayInputStream("content".getBytes())) + .execute(); + + assertThat(nameCaptor.getValue()).isEqualTo("toBeCreated"); + assertThat(overwriteCaptor.getValue()).isTrue(); + assertThat(contentCaptor).contains("content"); + } + + @Test + void shouldExecuteCreateMultipleTimes() throws IOException { + ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); + List contentCaptor = new ArrayList<>(); + doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any(), anyBoolean()); + + initCommand() + .createFile("toBeCreated_1").withData(new ByteArrayInputStream("content_1".getBytes())) + .createFile("toBeCreated_2").withData(new ByteArrayInputStream("content_2".getBytes())) + .execute(); + + List createdNames = nameCaptor.getAllValues(); + assertThat(createdNames.get(0)).isEqualTo("toBeCreated_1"); + assertThat(createdNames.get(1)).isEqualTo("toBeCreated_2"); + assertThat(contentCaptor).contains("content_1", "content_2"); + } + + @Test + void shouldExecuteModify() throws IOException { + ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); + List contentCaptor = new ArrayList<>(); + doAnswer(new ExtractContent(contentCaptor)).when(worker).modify(nameCaptor.capture(), any()); + + initCommand() + .modifyFile("toBeModified").withData(ByteSource.wrap("content".getBytes())) + .execute(); + + assertThat(nameCaptor.getValue()).isEqualTo("toBeModified"); + assertThat(contentCaptor).contains("content"); + } + + @Test + void shouldDeleteTemporaryFiles(@TempDir Path temp) throws IOException { + ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor fileCaptor = ArgumentCaptor.forClass(File.class); + doNothing().when(worker).modify(nameCaptor.capture(), fileCaptor.capture()); + + initCommand() + .modifyFile("toBeModified").withData(ByteSource.wrap("content".getBytes())) + .execute(); + + assertThat(Files.list(temp)).isEmpty(); + } + + @Test + void shouldUseMailFromUser() throws IOException { + initCommand() + .modifyFile("toBeModified").withData(ByteSource.wrap("content".getBytes())) + .execute(); + + verify(command).execute(argThat(modifyCommandRequest -> { + assertThat(modifyCommandRequest.getAuthor().getMail()).isEqualTo("dent@hitchhiker.com"); + return true; + })); + } + } + + @Nested + class WithUserWithoutMail { + + @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; + })); + } + } } diff --git a/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceFactoryTest.java b/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceFactoryTest.java index 492d23dec8..d77d0d072b 100644 --- a/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceFactoryTest.java +++ b/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceFactoryTest.java @@ -46,6 +46,7 @@ import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.spi.RepositoryServiceProvider; import sonia.scm.repository.spi.RepositoryServiceResolver; import sonia.scm.repository.work.WorkdirProvider; +import sonia.scm.user.EMail; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -56,9 +57,6 @@ import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class RepositoryServiceFactoryTest { - @Mock - private ScmConfiguration configuration; - @Mock(answer = Answers.RETURNS_MOCKS) private CacheManager cacheManager; @@ -94,8 +92,9 @@ class RepositoryServiceFactoryTest { builder.add(repositoryServiceResolver); } return new RepositoryServiceFactory( - configuration, cacheManager, repositoryManager, builder.build(), - preProcessorUtil, ImmutableSet.of(), workdirProvider, eventBus + cacheManager, repositoryManager, builder.build(), + preProcessorUtil, ImmutableSet.of(), workdirProvider, + new EMail(new ScmConfiguration()), eventBus ); } diff --git a/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceTest.java b/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceTest.java index 901666cb87..8394e3c582 100644 --- a/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceTest.java +++ b/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceTest.java @@ -21,13 +21,15 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.api; import org.junit.Test; +import sonia.scm.config.ScmConfiguration; import sonia.scm.repository.Repository; import sonia.scm.repository.spi.HttpScmProtocol; import sonia.scm.repository.spi.RepositoryServiceProvider; +import sonia.scm.user.EMail; import javax.servlet.ServletConfig; import javax.servlet.http.HttpServletRequest; @@ -46,9 +48,11 @@ public class RepositoryServiceTest { private final RepositoryServiceProvider provider = mock(RepositoryServiceProvider.class); private final Repository repository = new Repository("", "git", "space", "repo"); + private final EMail eMail = new EMail(new ScmConfiguration()); + @Test 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 supportedProtocols = repositoryService.getSupportedProtocols(); assertThat(sizeOf(supportedProtocols.collect(Collectors.toList()))).isEqualTo(1); @@ -56,7 +60,7 @@ public class RepositoryServiceTest { @Test 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); @@ -65,11 +69,9 @@ public class RepositoryServiceTest { @Test 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, () -> { - repositoryService.getProtocol(UnknownScmProtocol.class); - }); + assertThrows(IllegalArgumentException.class, () -> repositoryService.getProtocol(UnknownScmProtocol.class)); } private static class DummyHttpProtocol extends HttpScmProtocol { diff --git a/scm-core/src/test/java/sonia/scm/repository/util/AuthorUtilTest.java b/scm-core/src/test/java/sonia/scm/repository/util/AuthorUtilTest.java new file mode 100644 index 0000000000..08a49cea44 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/util/AuthorUtilTest.java @@ -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; + } + } + +} diff --git a/scm-core/src/test/java/sonia/scm/user/EMailTest.java b/scm-core/src/test/java/sonia/scm/user/EMailTest.java new file mode 100644 index 0000000000..49842223ba --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/user/EMailTest.java @@ -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"); + } +} diff --git a/scm-ui/ui-components/src/repos/CommitAuthor.tsx b/scm-ui/ui-components/src/repos/CommitAuthor.tsx new file mode 100644 index 0000000000..f845582267 --- /dev/null +++ b/scm-ui/ui-components/src/repos/CommitAuthor.tsx @@ -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 = ({ me }) => { + const [t] = useTranslation("repos"); + + const mail = me.mail ? me.mail : me.fallbackMail; + + return ( + <> + {!me.mail && {t("commit.commitAuthor.noMail")}} + + {t("commit.commitAuthor.author")} {`${me.displayName} <${mail}>`} + + + ); +}; + +const mapStateToProps = (state: any) => { + const { auth } = state; + const me = auth.me; + + return { + me + }; +}; + +export default connect(mapStateToProps)(CommitAuthor); diff --git a/scm-ui/ui-components/src/repos/index.ts b/scm-ui/ui-components/src/repos/index.ts index 36b26495b8..f33ffbc4cb 100644 --- a/scm-ui/ui-components/src/repos/index.ts +++ b/scm-ui/ui-components/src/repos/index.ts @@ -52,6 +52,7 @@ export { default as RepositoryAvatar } from "./RepositoryAvatar"; export { default as RepositoryEntry } from "./RepositoryEntry"; export { default as RepositoryEntryLink } from "./RepositoryEntryLink"; export { default as JumpToFileButton } from "./JumpToFileButton"; +export { default as CommitAuthor } from "./CommitAuthor"; export { File, diff --git a/scm-ui/ui-types/src/Config.ts b/scm-ui/ui-types/src/Config.ts index 667aa7b199..92b678e491 100644 --- a/scm-ui/ui-types/src/Config.ts +++ b/scm-ui/ui-types/src/Config.ts @@ -48,5 +48,6 @@ export type Config = { namespaceStrategy: string; loginInfoUrl: string; releaseFeedUrl: string; + mailDomainName: string; _links: Links; }; diff --git a/scm-ui/ui-types/src/Me.ts b/scm-ui/ui-types/src/Me.ts index 38a3a23278..d8595e0771 100644 --- a/scm-ui/ui-types/src/Me.ts +++ b/scm-ui/ui-types/src/Me.ts @@ -27,7 +27,8 @@ import { Links } from "./hal"; export type Me = { name: string; displayName: string; - mail: string; + mail?: string; + fallbackMail?: string; groups: string[]; _links: Links; }; diff --git a/scm-ui/ui-types/src/User.ts b/scm-ui/ui-types/src/User.ts index 43ff64b9fa..29009b5d0d 100644 --- a/scm-ui/ui-types/src/User.ts +++ b/scm-ui/ui-types/src/User.ts @@ -27,13 +27,13 @@ import { Links } from "./hal"; export type DisplayedUser = { id: string; displayName: string; - mail: string; + mail?: string; }; export type User = { displayName: string; name: string; - mail: string; + mail?: string; password: string; active: boolean; type?: string; diff --git a/scm-ui/ui-webapp/public/locales/de/config.json b/scm-ui/ui-webapp/public/locales/de/config.json index ad749c12c8..24830f2f4d 100644 --- a/scm-ui/ui-webapp/public/locales/de/config.json +++ b/scm-ui/ui-webapp/public/locales/de/config.json @@ -47,6 +47,7 @@ "skip-failed-authenticators": "Fehlgeschlagene Authentifizierer überspringen", "plugin-url": "Plugin Center URL", "release-feed-url": "Release Feed URL", + "mail-domain-name": "Fallback E-Mail Domain Name", "enabled-xsrf-protection": "XSRF Protection aktivieren", "namespace-strategy": "Namespace Strategie", "login-info-url": "Login Info URL" @@ -62,6 +63,7 @@ "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", "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.", "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.", diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json index 988a181ee8..d3ec72b23f 100644 --- a/scm-ui/ui-webapp/public/locales/de/repos.json +++ b/scm-ui/ui-webapp/public/locales/de/repos.json @@ -135,6 +135,12 @@ "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": { "subtitle": "Repository bearbeiten", "submit": "Speichern", @@ -273,4 +279,4 @@ "clickHere": "Klicken Sie hier um Ihre Datei hochzuladen.", "dragAndDrop": "Sie können Ihre Datei auch direkt in die Dropzone ziehen." } -} +}, diff --git a/scm-ui/ui-webapp/public/locales/en/config.json b/scm-ui/ui-webapp/public/locales/en/config.json index 1150dd4e3d..6cb06ecb25 100644 --- a/scm-ui/ui-webapp/public/locales/en/config.json +++ b/scm-ui/ui-webapp/public/locales/en/config.json @@ -47,6 +47,7 @@ "skip-failed-authenticators": "Skip Failed Authenticators", "plugin-url": "Plugin Center URL", "release-feed-url": "Release Feed URL", + "mail-domain-name": "Fallback Mail Domain Name", "enabled-xsrf-protection": "Enabled XSRF Protection", "namespace-strategy": "Namespace Strategy", "login-info-url": "Login Info URL" @@ -62,6 +63,7 @@ "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", "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.", "disableGroupingGridHelpText": "Disable repository Groups. A complete page reload is required after a change of this value.", "allowAnonymousAccessHelpText": "Anonymous users have access on granted repositories.", diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json index 345bbcbd1c..5f83ad7ba1 100644 --- a/scm-ui/ui-webapp/public/locales/en/repos.json +++ b/scm-ui/ui-webapp/public/locales/en/repos.json @@ -135,6 +135,12 @@ "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": { "subtitle": "Edit Repository", "submit": "Save", diff --git a/scm-ui/ui-webapp/src/admin/components/form/ConfigForm.tsx b/scm-ui/ui-webapp/src/admin/components/form/ConfigForm.tsx index f412acf8e8..c284df3180 100644 --- a/scm-ui/ui-webapp/src/admin/components/form/ConfigForm.tsx +++ b/scm-ui/ui-webapp/src/admin/components/form/ConfigForm.tsx @@ -144,6 +144,7 @@ class ConfigForm extends React.Component { skipFailedAuthenticators={config.skipFailedAuthenticators} pluginUrl={config.pluginUrl} releaseFeedUrl={config.releaseFeedUrl} + mailDomainName={config.mailDomainName} enabledXsrfProtection={config.enabledXsrfProtection} namespaceStrategy={config.namespaceStrategy} onChange={(isValid, changedValue, name) => this.onChange(isValid, changedValue, name)} diff --git a/scm-ui/ui-webapp/src/admin/components/form/GeneralSettings.tsx b/scm-ui/ui-webapp/src/admin/components/form/GeneralSettings.tsx index 580a3546da..e9ed50a3b5 100644 --- a/scm-ui/ui-webapp/src/admin/components/form/GeneralSettings.tsx +++ b/scm-ui/ui-webapp/src/admin/components/form/GeneralSettings.tsx @@ -36,6 +36,7 @@ type Props = WithTranslation & { skipFailedAuthenticators: boolean; pluginUrl: string; releaseFeedUrl: string; + mailDomainName: string; enabledXsrfProtection: boolean; namespaceStrategy: string; namespaceStrategies?: NamespaceStrategies; @@ -51,6 +52,7 @@ class GeneralSettings extends React.Component { loginInfoUrl, pluginUrl, releaseFeedUrl, + mailDomainName, enabledXsrfProtection, anonymousMode, namespaceStrategy, @@ -129,7 +131,7 @@ class GeneralSettings extends React.Component {
    -
    +
    { helpText={t("help.releaseFeedUrlHelpText")} />
    +
    + +
    ); @@ -164,6 +175,9 @@ class GeneralSettings extends React.Component { handleReleaseFeedUrlChange = (value: string) => { this.props.onChange(true, value, "releaseFeedUrl"); }; + handleMailDomainNameChange = (value: string) => { + this.props.onChange(true, value, "mailDomainName"); + }; } export default withTranslation("config")(GeneralSettings); diff --git a/scm-ui/ui-webapp/src/users/components/UserForm.tsx b/scm-ui/ui-webapp/src/users/components/UserForm.tsx index cd0856fa84..9c78ef8476 100644 --- a/scm-ui/ui-webapp/src/users/components/UserForm.tsx +++ b/scm-ui/ui-webapp/src/users/components/UserForm.tsx @@ -113,8 +113,7 @@ class UserForm extends React.Component { this.editUserComponentsAreUnchanged() || this.state.mailValidationError || this.state.displayNameValidationError || - this.isFalsy(user.displayName) || - this.isFalsy(user.mail) + this.isFalsy(user.displayName) ); }; @@ -152,6 +151,7 @@ class UserForm extends React.Component { // edit existing user subtitle = ; } + return ( <> {subtitle} @@ -218,7 +218,7 @@ class UserForm extends React.Component { handleEmailChange = (mail: string) => { this.setState({ - mailValidationError: !validator.isMailValid(mail), + mailValidationError: !!mail && !validator.isMailValid(mail), user: { ...this.state.user, mail diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java index bec72a552b..f7025f141f 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.api.v2.resources; import de.otto.edison.hal.HalRepresentation; @@ -59,6 +59,7 @@ public class ConfigDto extends HalRepresentation { private String namespaceStrategy; private String loginInfoUrl; private String releaseFeedUrl; + private String mailDomainName; @Override @SuppressWarnings("squid:S1185") // We want to have this method available in this package diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDto.java index 968d892536..b4e011f7bb 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDto.java @@ -21,9 +21,10 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.api.v2.resources; +import com.fasterxml.jackson.annotation.JsonInclude; import de.otto.edison.hal.Embedded; import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.Links; @@ -40,7 +41,10 @@ public class MeDto extends HalRepresentation { private String name; private String displayName; + @JsonInclude(JsonInclude.Include.NON_NULL) private String mail; + @JsonInclude(JsonInclude.Include.NON_NULL) + private String fallbackMail; private Set groups; MeDto(Links links, Embedded embedded) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDtoFactory.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDtoFactory.java index 2101ba2ffb..ff3914654d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDtoFactory.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDtoFactory.java @@ -24,12 +24,14 @@ package sonia.scm.api.v2.resources; +import com.google.common.base.Strings; import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Links; import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.Subject; import sonia.scm.group.GroupCollector; +import sonia.scm.user.EMail; import sonia.scm.user.User; import sonia.scm.user.UserManager; import sonia.scm.user.UserPermissions; @@ -46,12 +48,14 @@ public class MeDtoFactory extends HalAppenderMapper { private final ResourceLinks resourceLinks; private final UserManager userManager; private final GroupCollector groupCollector; + private final EMail eMail; @Inject - public MeDtoFactory(ResourceLinks resourceLinks, UserManager userManager, GroupCollector groupCollector) { + public MeDtoFactory(ResourceLinks resourceLinks, UserManager userManager, GroupCollector groupCollector, EMail eMail) { this.resourceLinks = resourceLinks; this.userManager = userManager; this.groupCollector = groupCollector; + this.eMail = eMail; } public MeDto create() { @@ -61,6 +65,7 @@ public class MeDtoFactory extends HalAppenderMapper { MeDto dto = createDto(user); mapUserProperties(user, dto); mapGroups(user, dto); + setGeneratedMail(user, dto); return dto; } @@ -79,6 +84,12 @@ public class MeDtoFactory extends HalAppenderMapper { 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) { Links.Builder linksBuilder = linkingTo().self(resourceLinks.me().self()); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserDto.java index 0e4e9b34c2..027cc59621 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserDto.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.api.v2.resources; import com.fasterxml.jackson.annotation.JsonInclude; @@ -46,7 +46,8 @@ public class UserDto extends HalRepresentation { private String displayName; @JsonInclude(JsonInclude.Include.NON_NULL) private Instant lastModified; - @NotEmpty @Email + @JsonInclude(JsonInclude.Include.NON_NULL) + @Email private String mail; @Pattern(regexp = ValidationUtil.REGEX_NAME) private String name; diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java index b924769b6c..fcd54bac7d 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java @@ -251,6 +251,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector builder.add(getGroupAutocompletePermission()); builder.add(getChangeOwnPasswordPermission(user)); builder.add(getApiKeyPermission(user)); + builder.add(getPublicKeyPermission(user)); } SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(ImmutableSet.of(Role.USER)); @@ -267,6 +268,10 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector return UserPermissions.changePassword(user).asShiroString(); } + private String getPublicKeyPermission(User user) { + return UserPermissions.changePublicKeys(user).asShiroString(); + } + private String getApiKeyPermission(User user) { return UserPermissions.changeApiKeys(user).asShiroString(); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigDtoToScmConfigurationMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigDtoToScmConfigurationMapperTest.java index 3e01a1e4cc..f938c7f655 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigDtoToScmConfigurationMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigDtoToScmConfigurationMapperTest.java @@ -75,6 +75,7 @@ public class ConfigDtoToScmConfigurationMapperTest { assertTrue(config.isEnabledXsrfProtection()); assertEquals("username", config.getNamespaceStrategy()); assertEquals("https://scm-manager.org/login-info", config.getLoginInfoUrl()); + assertEquals("hitchhiker.mail", config.getMailDomainName()); } @Test @@ -113,6 +114,7 @@ public class ConfigDtoToScmConfigurationMapperTest { configDto.setEnabledXsrfProtection(true); configDto.setNamespaceStrategy("username"); configDto.setLoginInfoUrl("https://scm-manager.org/login-info"); + configDto.setMailDomainName("hitchhiker.mail"); return configDto; } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeDtoFactoryTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeDtoFactoryTest.java index 901d625579..ea539c4631 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeDtoFactoryTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeDtoFactoryTest.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.api.v2.resources; import com.google.common.collect.ImmutableSet; @@ -38,9 +38,9 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import sonia.scm.SCMContext; import sonia.scm.group.GroupCollector; +import sonia.scm.user.EMail; import sonia.scm.user.User; import sonia.scm.user.UserManager; -import sonia.scm.user.UserPermissions; import sonia.scm.user.UserTestData; import java.net.URI; @@ -65,13 +65,16 @@ class MeDtoFactoryTest { @Mock private Subject subject; + @Mock + private EMail eMail; + private MeDtoFactory meDtoFactory; @BeforeEach void setUpContext() { ThreadContext.bind(subject); ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); - meDtoFactory = new MeDtoFactory(resourceLinks, userManager, groupCollector); + meDtoFactory = new MeDtoFactory(resourceLinks, userManager, groupCollector, eMail); } @AfterEach @@ -235,4 +238,17 @@ class MeDtoFactoryTest { MeDto dto = meDtoFactory.create(); 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"); + } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java index cef98c4062..49769bf1b3 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java @@ -106,6 +106,7 @@ public class ScmConfigurationToConfigDtoMapperTest { assertEquals("username", dto.getNamespaceStrategy()); assertEquals("https://scm-manager.org/login-info", dto.getLoginInfoUrl()); 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("update").get().getHref()); diff --git a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java index 93cce78932..16015187bd 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java @@ -167,8 +167,8 @@ public class DefaultAuthorizationCollectorTest { AuthorizationInfo authInfo = collector.collect(); assertThat(authInfo.getRoles(), Matchers.contains(Role.USER)); - assertThat(authInfo.getStringPermissions(), hasSize(5)); - assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:autocomplete", "group:autocomplete", "user:changePassword:trillian", "user:read:trillian", "user:changeApiKeys:trillian")); + assertThat(authInfo.getStringPermissions(), hasSize(6)); + assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:autocomplete", "group:autocomplete", "user:changePassword:trillian", "user:read:trillian", "user:changeApiKeys:trillian", "user:changePublicKeys:trillian")); assertThat(authInfo.getObjectPermissions(), nullValue()); } @@ -212,7 +212,7 @@ public class DefaultAuthorizationCollectorTest { AuthorizationInfo authInfo = collector.collect(); assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER)); assertThat(authInfo.getObjectPermissions(), nullValue()); - assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:autocomplete", "group:autocomplete", "user:changePassword:trillian", "repository:read,pull:one", "repository:read,pull,push:two", "user:read:trillian", "user:changeApiKeys:trillian")); + assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:autocomplete", "group:autocomplete", "user:changePassword:trillian", "repository:read,pull:one", "repository:read,pull,push:two", "user:read:trillian", "user:changeApiKeys:trillian", "user:changePublicKeys:trillian")); } /** @@ -244,7 +244,7 @@ public class DefaultAuthorizationCollectorTest { AuthorizationInfo authInfo = collector.collect(); assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER)); assertThat(authInfo.getObjectPermissions(), nullValue()); - assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:autocomplete", "group:autocomplete", "user:changePassword:trillian", "repository:read,pull:one", "repository:read,pull,push:two", "user:read:trillian", "user:changeApiKeys:trillian")); + assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:autocomplete", "group:autocomplete", "user:changePassword:trillian", "repository:read,pull:one", "repository:read,pull,push:two", "user:read:trillian", "user:changeApiKeys:trillian", "user:changePublicKeys:trillian")); } /** @@ -288,7 +288,9 @@ public class DefaultAuthorizationCollectorTest { "repository:system:one", "repository:group:two", "user:read:trillian", - "user:changeApiKeys:trillian")); + "user:changeApiKeys:trillian", + "user:changePublicKeys:trillian" + )); } /** @@ -335,7 +337,7 @@ public class DefaultAuthorizationCollectorTest { AuthorizationInfo authInfo = collector.collect(); assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER)); assertThat(authInfo.getObjectPermissions(), nullValue()); - assertThat(authInfo.getStringPermissions(), containsInAnyOrder("one:one", "two:two", "user:read:trillian", "user:autocomplete", "group:autocomplete", "user:changePassword:trillian", "user:changeApiKeys:trillian")); + assertThat(authInfo.getStringPermissions(), containsInAnyOrder("one:one", "two:two", "user:read:trillian", "user:autocomplete", "group:autocomplete", "user:changePassword:trillian", "user:changeApiKeys:trillian", "user:changePublicKeys:trillian")); } private void authenticate(User user, String group, String... groups) {