]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-5934 Create Java user service to create a user
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Tue, 16 Dec 2014 08:36:37 +0000 (09:36 +0100)
committerJulien Lancelot <julien.lancelot@sonarsource.com>
Tue, 16 Dec 2014 16:25:12 +0000 (17:25 +0100)
32 files changed:
server/sonar-server/src/main/java/org/sonar/server/db/DbClient.java
server/sonar-server/src/main/java/org/sonar/server/exceptions/Message.java
server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java
server/sonar-server/src/main/java/org/sonar/server/user/NewUser.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/user/ReactivationException.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/user/UserCreator.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/user/UserService.java
server/sonar-server/src/main/java/org/sonar/server/user/db/UserDao.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/user/db/UserGroupDao.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/util/Validation.java
server/sonar-server/src/test/java/org/sonar/server/user/UserCreatorTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/user/UserServiceMediumTest.java
server/sonar-server/src/test/java/org/sonar/server/user/db/UserDaoTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/user/db/UserGroupDaoTest.java [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/user/UserCreatorTest/associate_default_groups_when_reactivating_user.xml [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/user/UserCreatorTest/fail_to_create_user_if_already_exists.xml [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/user/UserCreatorTest/fail_to_create_user_if_already_exists_but_inactive.xml [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/user/UserCreatorTest/reactivate_user.xml [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/user/db/UserDaoTest/select_by_login.xml [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/user/db/UserGroupDaoTest/insert-result.xml [new file with mode: 0644]
server/sonar-web/src/main/webapp/WEB-INF/app/controllers/users_controller.rb
sonar-core/src/main/java/org/sonar/core/persistence/MyBatis.java
sonar-core/src/main/java/org/sonar/core/user/UserDao.java
sonar-core/src/main/java/org/sonar/core/user/UserDto.java
sonar-core/src/main/java/org/sonar/core/user/UserGroupDto.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/user/UserGroupMapper.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/user/UserMapper.java
sonar-core/src/main/resources/org/sonar/core/user/UserGroupMapper.xml [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/core/user/UserMapper.xml
sonar-core/src/main/resources/org/sonar/l10n/core.properties
sonar-core/src/test/java/org/sonar/core/user/UserDaoTest.java
sonar-core/src/test/resources/org/sonar/core/user/UserDaoTest/update_user.xml [new file with mode: 0644]

index e40789d9a8102d0cba3856083b61543026ea2e93..4689180ce02f54e7dd1cde804008132aaa16401a 100644 (file)
@@ -32,7 +32,6 @@ import org.sonar.core.source.db.FileSourceDao;
 import org.sonar.core.technicaldebt.db.CharacteristicDao;
 import org.sonar.core.template.LoadedTemplateDao;
 import org.sonar.core.user.AuthorizationDao;
-import org.sonar.core.user.UserDao;
 import org.sonar.server.activity.db.ActivityDao;
 import org.sonar.server.component.db.ComponentDao;
 import org.sonar.server.component.db.SnapshotDao;
@@ -46,6 +45,7 @@ import org.sonar.server.measure.persistence.MetricDao;
 import org.sonar.server.qualityprofile.db.ActiveRuleDao;
 import org.sonar.server.rule.db.RuleDao;
 import org.sonar.server.user.db.GroupDao;
+import org.sonar.server.user.db.UserDao;
 
 import java.sql.Connection;
 import java.sql.PreparedStatement;
index d821b6f246a154a317b989f0b079ffc5ce255670..38e6a6379f4ee305a8e79c4204ff4ac25e6d432b 100644 (file)
@@ -22,6 +22,8 @@ package org.sonar.server.exceptions;
 import com.google.common.base.Objects;
 import org.apache.commons.lang.StringUtils;
 
+import java.util.Arrays;
+
 public class Message {
 
   private final String key;
@@ -44,6 +46,35 @@ public class Message {
     return new Message(StringUtils.defaultString(l10nKey), l10nParams);
   }
 
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+
+    Message message = (Message) o;
+
+    if (!key.equals(message.key)) {
+      return false;
+    }
+    // Probably incorrect - comparing Object[] arrays with Arrays.equals
+    if (!Arrays.equals(params, message.params)) {
+      return false;
+    }
+
+    return true;
+  }
+
+  @Override
+  public int hashCode() {
+    int result = key.hashCode();
+    result = 31 * result + (params != null ? Arrays.hashCode(params) : 0);
+    return result;
+  }
+
   @Override
   public String toString() {
     return Objects.toStringHelper(this)
index 306c3a007d192d645449bb6b75a0b8461971035f..ef0bdb16177d0a8f60e655b9f44652b8eb6867c4 100644 (file)
@@ -54,13 +54,7 @@ import org.sonar.core.measure.db.MeasureFilterDao;
 import org.sonar.core.metric.DefaultMetricFinder;
 import org.sonar.core.notification.DefaultNotificationManager;
 import org.sonar.core.permission.PermissionFacade;
-import org.sonar.core.persistence.DaoUtils;
-import org.sonar.core.persistence.DatabaseVersion;
-import org.sonar.core.persistence.DefaultDatabase;
-import org.sonar.core.persistence.MyBatis;
-import org.sonar.core.persistence.PreviewDatabaseFactory;
-import org.sonar.core.persistence.SemaphoreUpdater;
-import org.sonar.core.persistence.SemaphoresImpl;
+import org.sonar.core.persistence.*;
 import org.sonar.core.preview.PreviewCache;
 import org.sonar.core.profiling.Profiling;
 import org.sonar.core.purge.PurgeProfiler;
@@ -86,11 +80,7 @@ import org.sonar.server.activity.index.ActivityNormalizer;
 import org.sonar.server.activity.ws.ActivitiesWebService;
 import org.sonar.server.activity.ws.ActivityMapping;
 import org.sonar.server.authentication.ws.AuthenticationWs;
-import org.sonar.server.batch.BatchIndex;
-import org.sonar.server.batch.BatchWs;
-import org.sonar.server.batch.GlobalReferentialsAction;
-import org.sonar.server.batch.ProjectReferentialsAction;
-import org.sonar.server.batch.UploadReportAction;
+import org.sonar.server.batch.*;
 import org.sonar.server.charts.ChartFactory;
 import org.sonar.server.component.ComponentCleanerService;
 import org.sonar.server.component.ComponentService;
@@ -98,25 +88,10 @@ import org.sonar.server.component.DefaultComponentFinder;
 import org.sonar.server.component.DefaultRubyComponentService;
 import org.sonar.server.component.db.ComponentDao;
 import org.sonar.server.component.db.SnapshotDao;
-import org.sonar.server.component.ws.ComponentAppAction;
-import org.sonar.server.component.ws.ComponentsWs;
-import org.sonar.server.component.ws.EventsWs;
-import org.sonar.server.component.ws.ProjectsWs;
-import org.sonar.server.component.ws.ResourcesWs;
-import org.sonar.server.computation.AnalysisReportQueue;
-import org.sonar.server.computation.AnalysisReportService;
-import org.sonar.server.computation.AnalysisReportTaskCleaner;
-import org.sonar.server.computation.AnalysisReportTaskLauncher;
-import org.sonar.server.computation.ComputationService;
+import org.sonar.server.component.ws.*;
+import org.sonar.server.computation.*;
 import org.sonar.server.computation.db.AnalysisReportDao;
-import org.sonar.server.computation.step.ComponentIndexationInDatabaseStep;
-import org.sonar.server.computation.step.ComputationStepRegistry;
-import org.sonar.server.computation.step.DataCleanerStep;
-import org.sonar.server.computation.step.DigestReportStep;
-import org.sonar.server.computation.step.IndexProjectIssuesStep;
-import org.sonar.server.computation.step.InvalidatePreviewCacheStep;
-import org.sonar.server.computation.step.SwitchSnapshotStep;
-import org.sonar.server.computation.step.SynchronizeProjectPermissionsStep;
+import org.sonar.server.computation.step.*;
 import org.sonar.server.computation.ws.ActiveAnalysisReportsAction;
 import org.sonar.server.computation.ws.AnalysisReportHistorySearchAction;
 import org.sonar.server.computation.ws.AnalysisReportWebService;
@@ -132,14 +107,7 @@ import org.sonar.server.db.DbClient;
 import org.sonar.server.db.EmbeddedDatabaseFactory;
 import org.sonar.server.db.migrations.DatabaseMigrations;
 import org.sonar.server.db.migrations.DatabaseMigrator;
-import org.sonar.server.debt.DebtCharacteristicsXMLImporter;
-import org.sonar.server.debt.DebtModelBackup;
-import org.sonar.server.debt.DebtModelLookup;
-import org.sonar.server.debt.DebtModelOperations;
-import org.sonar.server.debt.DebtModelPluginRepository;
-import org.sonar.server.debt.DebtModelService;
-import org.sonar.server.debt.DebtModelXMLExporter;
-import org.sonar.server.debt.DebtRulesXMLImporter;
+import org.sonar.server.debt.*;
 import org.sonar.server.design.FileDesignWidget;
 import org.sonar.server.duplication.ws.DuplicationsJsonWriter;
 import org.sonar.server.duplication.ws.DuplicationsParser;
@@ -147,33 +115,14 @@ import org.sonar.server.duplication.ws.DuplicationsWs;
 import org.sonar.server.es.EsClient;
 import org.sonar.server.es.IndexCreator;
 import org.sonar.server.es.IndexRegistry;
-import org.sonar.server.issue.ActionService;
-import org.sonar.server.issue.AddTagsAction;
-import org.sonar.server.issue.AssignAction;
-import org.sonar.server.issue.CommentAction;
-import org.sonar.server.issue.InternalRubyIssueService;
-import org.sonar.server.issue.IssueBulkChangeService;
-import org.sonar.server.issue.IssueChangelogFormatter;
-import org.sonar.server.issue.IssueChangelogService;
-import org.sonar.server.issue.IssueCommentService;
-import org.sonar.server.issue.IssueQueryService;
-import org.sonar.server.issue.IssueService;
-import org.sonar.server.issue.PlanAction;
-import org.sonar.server.issue.RemoveTagsAction;
-import org.sonar.server.issue.ServerIssueStorage;
-import org.sonar.server.issue.SetSeverityAction;
-import org.sonar.server.issue.TransitionAction;
+import org.sonar.server.issue.*;
 import org.sonar.server.issue.actionplan.ActionPlanService;
 import org.sonar.server.issue.actionplan.ActionPlanWs;
 import org.sonar.server.issue.db.IssueDao;
 import org.sonar.server.issue.filter.IssueFilterService;
 import org.sonar.server.issue.filter.IssueFilterWriter;
 import org.sonar.server.issue.filter.IssueFilterWs;
-import org.sonar.server.issue.index.IssueAuthorizationIndexer;
-import org.sonar.server.issue.index.IssueIndex;
-import org.sonar.server.issue.index.IssueIndexDefinition;
-import org.sonar.server.issue.index.IssueIndexer;
-import org.sonar.server.issue.index.IssueNormalizer;
+import org.sonar.server.issue.index.*;
 import org.sonar.server.issue.ws.IssueActionsWriter;
 import org.sonar.server.issue.ws.IssueShowAction;
 import org.sonar.server.issue.ws.IssuesWs;
@@ -196,118 +145,34 @@ import org.sonar.server.platform.ws.L10nWs;
 import org.sonar.server.platform.ws.RestartHandler;
 import org.sonar.server.platform.ws.ServerWs;
 import org.sonar.server.platform.ws.SystemWs;
-import org.sonar.server.plugins.InstalledPluginReferentialFactory;
-import org.sonar.server.plugins.PluginDownloader;
-import org.sonar.server.plugins.ServerExtensionInstaller;
-import org.sonar.server.plugins.ServerPluginJarInstaller;
-import org.sonar.server.plugins.ServerPluginJarsInstaller;
-import org.sonar.server.plugins.ServerPluginRepository;
-import org.sonar.server.plugins.UpdateCenterClient;
-import org.sonar.server.plugins.UpdateCenterMatrixFactory;
+import org.sonar.server.plugins.*;
 import org.sonar.server.properties.ProjectSettingsFactory;
 import org.sonar.server.qualitygate.QgateProjectFinder;
 import org.sonar.server.qualitygate.QualityGates;
 import org.sonar.server.qualitygate.RegisterQualityGates;
-import org.sonar.server.qualitygate.ws.QGatesAppAction;
-import org.sonar.server.qualitygate.ws.QGatesCopyAction;
-import org.sonar.server.qualitygate.ws.QGatesCreateAction;
-import org.sonar.server.qualitygate.ws.QGatesCreateConditionAction;
-import org.sonar.server.qualitygate.ws.QGatesDeleteConditionAction;
-import org.sonar.server.qualitygate.ws.QGatesDeselectAction;
-import org.sonar.server.qualitygate.ws.QGatesDestroyAction;
-import org.sonar.server.qualitygate.ws.QGatesListAction;
-import org.sonar.server.qualitygate.ws.QGatesRenameAction;
-import org.sonar.server.qualitygate.ws.QGatesSearchAction;
-import org.sonar.server.qualitygate.ws.QGatesSelectAction;
-import org.sonar.server.qualitygate.ws.QGatesSetAsDefaultAction;
-import org.sonar.server.qualitygate.ws.QGatesShowAction;
-import org.sonar.server.qualitygate.ws.QGatesUnsetDefaultAction;
-import org.sonar.server.qualitygate.ws.QGatesUpdateConditionAction;
-import org.sonar.server.qualitygate.ws.QGatesWs;
-import org.sonar.server.qualityprofile.BuiltInProfiles;
-import org.sonar.server.qualityprofile.QProfileBackuper;
-import org.sonar.server.qualityprofile.QProfileCopier;
-import org.sonar.server.qualityprofile.QProfileExporters;
-import org.sonar.server.qualityprofile.QProfileFactory;
-import org.sonar.server.qualityprofile.QProfileLoader;
-import org.sonar.server.qualityprofile.QProfileLookup;
-import org.sonar.server.qualityprofile.QProfileProjectLookup;
-import org.sonar.server.qualityprofile.QProfileProjectOperations;
-import org.sonar.server.qualityprofile.QProfileReset;
-import org.sonar.server.qualityprofile.QProfileService;
-import org.sonar.server.qualityprofile.QProfiles;
-import org.sonar.server.qualityprofile.RegisterQualityProfiles;
-import org.sonar.server.qualityprofile.RuleActivator;
-import org.sonar.server.qualityprofile.RuleActivatorContextFactory;
+import org.sonar.server.qualitygate.ws.*;
+import org.sonar.server.qualityprofile.*;
 import org.sonar.server.qualityprofile.db.ActiveRuleDao;
 import org.sonar.server.qualityprofile.index.ActiveRuleIndex;
 import org.sonar.server.qualityprofile.index.ActiveRuleNormalizer;
-import org.sonar.server.qualityprofile.ws.BulkRuleActivationActions;
-import org.sonar.server.qualityprofile.ws.ProfilesWs;
-import org.sonar.server.qualityprofile.ws.QProfileRestoreBuiltInAction;
-import org.sonar.server.qualityprofile.ws.QProfilesWs;
-import org.sonar.server.qualityprofile.ws.RuleActivationActions;
-import org.sonar.server.rule.DefaultRuleFinder;
-import org.sonar.server.rule.DeprecatedRulesDefinitionLoader;
-import org.sonar.server.rule.RegisterRules;
-import org.sonar.server.rule.RubyRuleService;
-import org.sonar.server.rule.RuleCreator;
-import org.sonar.server.rule.RuleDefinitionsLoader;
-import org.sonar.server.rule.RuleDeleter;
-import org.sonar.server.rule.RuleOperations;
-import org.sonar.server.rule.RuleRepositories;
-import org.sonar.server.rule.RuleService;
-import org.sonar.server.rule.RuleUpdater;
+import org.sonar.server.qualityprofile.ws.*;
+import org.sonar.server.rule.*;
 import org.sonar.server.rule.db.RuleDao;
 import org.sonar.server.rule.index.RuleIndex;
 import org.sonar.server.rule.index.RuleNormalizer;
-import org.sonar.server.rule.ws.ActiveRuleCompleter;
-import org.sonar.server.rule.ws.AppAction;
-import org.sonar.server.rule.ws.DeleteAction;
-import org.sonar.server.rule.ws.RuleMapping;
-import org.sonar.server.rule.ws.RulesWebService;
-import org.sonar.server.rule.ws.SearchAction;
-import org.sonar.server.rule.ws.TagsAction;
-import org.sonar.server.rule.ws.UpdateAction;
-import org.sonar.server.search.IndexClient;
-import org.sonar.server.search.IndexQueue;
-import org.sonar.server.search.IndexSynchronizer;
-import org.sonar.server.search.SearchClient;
-import org.sonar.server.search.SearchHealth;
+import org.sonar.server.rule.ws.*;
+import org.sonar.server.search.*;
 import org.sonar.server.source.HtmlSourceDecorator;
 import org.sonar.server.source.IndexSourceLinesStep;
 import org.sonar.server.source.SourceService;
 import org.sonar.server.source.index.SourceLineIndex;
 import org.sonar.server.source.index.SourceLineIndexDefinition;
 import org.sonar.server.source.index.SourceLineIndexer;
-import org.sonar.server.source.ws.HashAction;
-import org.sonar.server.source.ws.IndexAction;
-import org.sonar.server.source.ws.LinesAction;
-import org.sonar.server.source.ws.RawAction;
-import org.sonar.server.source.ws.ScmAction;
-import org.sonar.server.source.ws.ScmWriter;
+import org.sonar.server.source.ws.*;
 import org.sonar.server.source.ws.ShowAction;
-import org.sonar.server.source.ws.SourcesWs;
-import org.sonar.server.startup.CleanPreviewAnalysisCache;
-import org.sonar.server.startup.CopyRequirementsFromCharacteristicsToRules;
-import org.sonar.server.startup.GeneratePluginIndex;
-import org.sonar.server.startup.JdbcDriverDeployer;
-import org.sonar.server.startup.LogServerId;
-import org.sonar.server.startup.RegisterDashboards;
-import org.sonar.server.startup.RegisterDebtModel;
-import org.sonar.server.startup.RegisterMetrics;
-import org.sonar.server.startup.RegisterNewMeasureFilters;
-import org.sonar.server.startup.RegisterPermissionTemplates;
-import org.sonar.server.startup.RegisterServletFilters;
-import org.sonar.server.startup.RenameDeprecatedPropertyKeys;
-import org.sonar.server.startup.ServerMetadataPersister;
+import org.sonar.server.startup.*;
 import org.sonar.server.test.CoverageService;
-import org.sonar.server.test.ws.CoverageShowAction;
-import org.sonar.server.test.ws.CoverageWs;
-import org.sonar.server.test.ws.TestsCoveredFilesAction;
-import org.sonar.server.test.ws.TestsShowAction;
-import org.sonar.server.test.ws.TestsTestCasesAction;
-import org.sonar.server.test.ws.TestsWs;
+import org.sonar.server.test.ws.*;
 import org.sonar.server.text.MacroInterpreter;
 import org.sonar.server.text.RubyTextService;
 import org.sonar.server.ui.JRubyI18n;
@@ -315,26 +180,16 @@ import org.sonar.server.ui.JRubyProfiling;
 import org.sonar.server.ui.PageDecorations;
 import org.sonar.server.ui.Views;
 import org.sonar.server.updatecenter.ws.UpdateCenterWs;
-import org.sonar.server.user.DefaultUserService;
-import org.sonar.server.user.DoPrivileged;
-import org.sonar.server.user.GroupMembershipFinder;
-import org.sonar.server.user.GroupMembershipService;
-import org.sonar.server.user.NewUserNotifier;
-import org.sonar.server.user.SecurityRealmFactory;
-import org.sonar.server.user.UserService;
+import org.sonar.server.user.*;
 import org.sonar.server.user.db.GroupDao;
+import org.sonar.server.user.db.UserDao;
+import org.sonar.server.user.db.UserGroupDao;
 import org.sonar.server.user.index.UserIndexDefinition;
 import org.sonar.server.user.index.UserIndexer;
 import org.sonar.server.user.ws.FavoritesWs;
 import org.sonar.server.user.ws.UserPropertiesWs;
 import org.sonar.server.user.ws.UsersWs;
-import org.sonar.server.util.BooleanTypeValidation;
-import org.sonar.server.util.FloatTypeValidation;
-import org.sonar.server.util.IntegerTypeValidation;
-import org.sonar.server.util.StringListTypeValidation;
-import org.sonar.server.util.StringTypeValidation;
-import org.sonar.server.util.TextTypeValidation;
-import org.sonar.server.util.TypeValidations;
+import org.sonar.server.util.*;
 import org.sonar.server.ws.ListingWs;
 import org.sonar.server.ws.WebServiceEngine;
 
@@ -397,6 +252,8 @@ class ServerComponents {
 
       // users
       GroupDao.class,
+      UserDao.class,
+      UserGroupDao.class,
 
       // dashboards
       DashboardDao.class,
@@ -616,6 +473,7 @@ class ServerComponents {
     pico.addSingleton(UserIndexDefinition.class);
     pico.addSingleton(UserIndexer.class);
     pico.addSingleton(UserService.class);
+    pico.addSingleton(UserCreator.class);
 
     // groups
     pico.addSingleton(GroupMembershipService.class);
diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/NewUser.java b/server/sonar-server/src/main/java/org/sonar/server/user/NewUser.java
new file mode 100644 (file)
index 0000000..833ecea
--- /dev/null
@@ -0,0 +1,114 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.user;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+
+import java.util.List;
+
+public class NewUser {
+
+  private String login;
+  private String name;
+  private String email;
+  private List<String> scmAccounts;
+
+  private String password;
+  private String passwordConfirmation;
+
+  private boolean preventReactivation = false;
+
+  private NewUser() {
+    // No direct call to this constructor
+  }
+
+  public NewUser setLogin(String login) {
+    this.login = login;
+    return this;
+  }
+
+  public String login() {
+    return login;
+  }
+
+  public String name() {
+    return name;
+  }
+
+  public NewUser setName(String name) {
+    this.name = name;
+    return this;
+  }
+
+  @CheckForNull
+  public String email() {
+    return email;
+  }
+
+  public NewUser setEmail(@Nullable String email) {
+    this.email = email;
+    return this;
+  }
+
+  public List<String> scmAccounts() {
+    return scmAccounts;
+  }
+
+  public NewUser setScmAccounts(List<String> scmAccounts) {
+    this.scmAccounts = scmAccounts;
+    return this;
+  }
+
+  public String password() {
+    return password;
+  }
+
+  public NewUser setPassword(String password) {
+    this.password = password;
+    return this;
+  }
+
+  public String passwordConfirmation() {
+    return passwordConfirmation;
+  }
+
+  public NewUser setPasswordConfirmation(String passwordConfirmation) {
+    this.passwordConfirmation = passwordConfirmation;
+    return this;
+  }
+
+  public boolean isPreventReactivation() {
+    return preventReactivation;
+  }
+
+  /**
+   * When true, if the user already exists in status disabled, an {@link org.sonar.server.user.ReactivationException} will be thrown
+   */
+  public NewUser setPreventReactivation(boolean preventReactivation) {
+    this.preventReactivation = preventReactivation;
+    return this;
+  }
+
+  public static NewUser create() {
+    return new NewUser();
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/ReactivationException.java b/server/sonar-server/src/main/java/org/sonar/server/user/ReactivationException.java
new file mode 100644 (file)
index 0000000..92b9fde
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.user;
+
+public class ReactivationException extends RuntimeException {
+
+  private String login;
+
+  public ReactivationException(String message, String login) {
+    super(message);
+    this.login = login;
+  }
+
+  public String login() {
+    return login;
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/UserCreator.java b/server/sonar-server/src/main/java/org/sonar/server/user/UserCreator.java
new file mode 100644 (file)
index 0000000..fa18565
--- /dev/null
@@ -0,0 +1,219 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.user;
+
+import com.google.common.base.Predicate;
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.config.Settings;
+import org.sonar.api.platform.NewUserHandler;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.text.CsvWriter;
+import org.sonar.core.persistence.DbSession;
+import org.sonar.core.user.GroupDto;
+import org.sonar.core.user.UserDto;
+import org.sonar.core.user.UserGroupDto;
+import org.sonar.server.db.DbClient;
+import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.exceptions.Message;
+import org.sonar.server.user.db.UserGroupDao;
+import org.sonar.server.util.Validation;
+
+import javax.annotation.Nullable;
+
+import java.io.StringWriter;
+import java.security.SecureRandom;
+import java.util.List;
+import java.util.Random;
+
+import static com.google.common.collect.Lists.newArrayList;
+
+public class UserCreator {
+
+  private static final String LOGIN = "Login";
+  private static final String PASSWORD_CONFIRMATION = "Password confirmation";
+  private static final String PASSWORD = "Password";
+  private static final String NAME = "Name";
+  private static final String EMAIL = "Email";
+
+  private final NewUserNotifier newUserNotifier;
+  private final Settings settings;
+  private final UserGroupDao userGroupDao;
+  private final DbClient dbClient;
+  private final System2 system2;
+
+  public UserCreator(NewUserNotifier newUserNotifier, Settings settings, UserGroupDao userGroupDao, DbClient dbClient, System2 system2) {
+    this.newUserNotifier = newUserNotifier;
+    this.settings = settings;
+    this.userGroupDao = userGroupDao;
+    this.dbClient = dbClient;
+    this.system2 = system2;
+  }
+
+  public void create(NewUser newUser) {
+    validate(newUser);
+
+    DbSession dbSession = dbClient.openSession(false);
+    try {
+      String login = newUser.login();
+      UserDto existingUser = dbClient.userDao().selectNullableByLogin(dbSession, login);
+      if (existingUser != null) {
+        updateExistingUser(dbSession, newUser);
+      } else {
+        createNewUser(dbSession, newUser);
+      }
+      dbSession.commit();
+      notifyNewUser(newUser);
+    } finally {
+      dbSession.close();
+    }
+  }
+
+  private static void validate(NewUser newUser) {
+    List<Message> messages = newArrayList();
+
+    validateLogin(newUser.login(), messages);
+    validateName(newUser.name(), messages);
+    if (newUser.email().length() >= 100) {
+      messages.add(Message.of(Validation.IS_TOO_LONG_MESSAGE, EMAIL, 100));
+    }
+    validatePassword(newUser, messages);
+
+    if (!messages.isEmpty()) {
+      throw new BadRequestException(messages);
+    }
+  }
+
+  private static void validateLogin(@Nullable String login, List<Message> messages) {
+    if (Strings.isNullOrEmpty(login)) {
+      messages.add(Message.of(Validation.CANT_BE_EMPTY_MESSAGE, LOGIN));
+    } else if (!login.matches("\\A\\w[\\w\\.\\-_@\\s]+\\z")) {
+      messages.add(Message.of("user.bad_login"));
+    } else if (login.length() <= 2) {
+      messages.add(Message.of(Validation.IS_TOO_SHORT_MESSAGE, LOGIN, 2));
+    } else if (login.length() >= 255) {
+      messages.add(Message.of(Validation.IS_TOO_LONG_MESSAGE, LOGIN, 255));
+    }
+  }
+
+  private static void validateName(@Nullable String name, List<Message> messages) {
+    if (Strings.isNullOrEmpty(name)) {
+      messages.add(Message.of(Validation.CANT_BE_EMPTY_MESSAGE, NAME));
+    } else if (name.length() >= 200) {
+      messages.add(Message.of(Validation.IS_TOO_LONG_MESSAGE, NAME, 200));
+    }
+  }
+
+  private static void validatePassword(NewUser newUser, List<Message> messages) {
+    if (Strings.isNullOrEmpty(newUser.password())) {
+      messages.add(Message.of(Validation.CANT_BE_EMPTY_MESSAGE, PASSWORD));
+    }
+    if (Strings.isNullOrEmpty(newUser.passwordConfirmation())) {
+      messages.add(Message.of(Validation.CANT_BE_EMPTY_MESSAGE, PASSWORD_CONFIRMATION));
+    }
+
+    if (!Strings.isNullOrEmpty(newUser.password()) && !Strings.isNullOrEmpty(newUser.passwordConfirmation())
+      && !StringUtils.equals(newUser.password(), newUser.passwordConfirmation())) {
+      messages.add(Message.of("user.password_doesnt_match_confirmation"));
+    }
+  }
+
+  private void createNewUser(DbSession dbSession, NewUser newUser) {
+    long now = system2.now();
+    UserDto userDto = new UserDto()
+      .setLogin(newUser.login())
+      .setName(newUser.name())
+      .setEmail(newUser.email())
+      .setActive(true)
+      .setScmAccounts(convertScmAccountsToCsv(newUser))
+      .setCreatedAt(now)
+      .setUpdatedAt(now);
+    setEncryptedPassWord(newUser, userDto);
+    dbClient.userDao().insert(dbSession, userDto);
+    addDefaultGroup(dbSession, userDto);
+  }
+
+  private void updateExistingUser(DbSession dbSession, NewUser newUser) {
+    String login = newUser.login();
+    UserDto existingUser = dbClient.userDao().selectNullableByLogin(dbSession, login);
+    if (existingUser != null) {
+      if (!existingUser.isActive()) {
+        if (newUser.isPreventReactivation()) {
+          throw new ReactivationException(String.format("A disabled user with the login '%s' already exists", login), login);
+        } else {
+          existingUser.setActive(true);
+          existingUser.setUpdatedAt(system2.now());
+          dbClient.userDao().update(dbSession, existingUser);
+          addDefaultGroup(dbSession, existingUser);
+        }
+      } else {
+        throw new IllegalArgumentException(String.format("A user with the login '%s' already exists", login));
+      }
+    }
+  }
+
+  private static void setEncryptedPassWord(NewUser newUser, UserDto userDto) {
+    Random random = new SecureRandom();
+    byte[] salt = new byte[32];
+    random.nextBytes(salt);
+    String saltHex = DigestUtils.sha1Hex(salt);
+    userDto.setSalt(saltHex);
+    userDto.setCryptedPassword(DigestUtils.sha1Hex("--" + saltHex + "--" + newUser.password() + "--"));
+  }
+
+  private static String convertScmAccountsToCsv(NewUser newUser) {
+    int size = newUser.scmAccounts().size();
+    StringWriter writer = new StringWriter(size);
+    CsvWriter csv = CsvWriter.of(writer);
+    csv.values(newUser.scmAccounts().toArray(new String[size]));
+    csv.close();
+    return writer.toString();
+  }
+
+  private void notifyNewUser(NewUser newUser) {
+    newUserNotifier.onNewUser(NewUserHandler.Context.builder()
+      .setLogin(newUser.login())
+      .setName(newUser.name())
+      .setEmail(newUser.email())
+      .build());
+  }
+
+  private void addDefaultGroup(DbSession dbSession, UserDto userDto) {
+    final String defaultGroup = settings.getString(CoreProperties.CORE_DEFAULT_GROUP);
+    if (defaultGroup == null) {
+      throw new IllegalStateException(String.format("The default group property '%s' is null", CoreProperties.CORE_DEFAULT_GROUP));
+    }
+    List<GroupDto> userGroups = dbClient.groupDao().findByUserLogin(dbSession, userDto.getLogin());
+    if (!Iterables.any(userGroups, new Predicate<GroupDto>() {
+      @Override
+      public boolean apply(@Nullable GroupDto input) {
+        return input != null && input.getKey().equals(defaultGroup);
+      }
+    })) {
+      GroupDto groupDto = dbClient.groupDao().getByKey(dbSession, defaultGroup);
+      userGroupDao.insert(dbSession, new UserGroupDto().setUserId(userDto.getId()).setGroupId(groupDto.getId()));
+    }
+  }
+
+}
index 8cf3ca10af857848a38c35f03b357a4464f6a1b3..e7f504f64a2d6b88b4946ed7f4d3e29ab1f74ef2 100644 (file)
 package org.sonar.server.user;
 
 import org.sonar.api.ServerComponent;
+import org.sonar.core.permission.GlobalPermissions;
 import org.sonar.server.user.index.UserIndexer;
 
 public class UserService implements ServerComponent {
 
   private final UserIndexer userIndexer;
+  private final UserCreator userCreator;
 
-  public UserService(UserIndexer userIndexer) {
+  public UserService(UserIndexer userIndexer, UserCreator userCreator) {
     this.userIndexer = userIndexer;
+    this.userCreator = userCreator;
+  }
+
+  public void create(NewUser newUser) {
+    checkPermission();
+    userCreator.create(newUser);
+    userIndexer.index();
   }
 
   public void index() {
     userIndexer.index();
   }
+
+  private void checkPermission() {
+    UserSession userSession = UserSession.get();
+    userSession.checkLoggedIn();
+    userSession.checkGlobalPermission(GlobalPermissions.SYSTEM_ADMIN);
+  }
 }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/db/UserDao.java b/server/sonar-server/src/main/java/org/sonar/server/user/db/UserDao.java
new file mode 100644 (file)
index 0000000..977c3c9
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.user.db;
+
+import org.sonar.api.utils.System2;
+import org.sonar.core.persistence.DaoComponent;
+import org.sonar.core.persistence.DbSession;
+import org.sonar.core.persistence.MyBatis;
+import org.sonar.core.user.UserDto;
+import org.sonar.core.user.UserMapper;
+import org.sonar.server.exceptions.NotFoundException;
+
+import javax.annotation.CheckForNull;
+
+public class UserDao extends org.sonar.core.user.UserDao implements DaoComponent {
+
+  public UserDao(MyBatis mybatis, System2 system2) {
+    super(mybatis, system2);
+  }
+
+  @CheckForNull
+  public UserDto selectNullableByLogin(DbSession session, String login) {
+    return mapper(session).selectByLogin(login);
+  }
+
+  public UserDto selectByLogin(DbSession session, String login) {
+    UserDto user = selectNullableByLogin(session, login);
+    if (user == null) {
+      throw new NotFoundException(String.format("User with login '%s' has not been found", login));
+    }
+    return user;
+  }
+
+  protected UserMapper mapper(DbSession session) {
+    return session.getMapper(UserMapper.class);
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/db/UserGroupDao.java b/server/sonar-server/src/main/java/org/sonar/server/user/db/UserGroupDao.java
new file mode 100644 (file)
index 0000000..60e29f3
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.user.db;
+
+import org.sonar.core.persistence.DaoComponent;
+import org.sonar.core.persistence.DbSession;
+import org.sonar.core.user.UserGroupDto;
+import org.sonar.core.user.UserGroupMapper;
+
+public class UserGroupDao implements DaoComponent {
+
+  public UserGroupDto insert(DbSession session, UserGroupDto dto) {
+    mapper(session).insert(dto);
+    return dto;
+  }
+
+  protected UserGroupMapper mapper(DbSession session) {
+    return session.getMapper(UserGroupMapper.class);
+  }
+}
index 6d9f1f471d4df110eeb9ab4b160150d1bbf3f214..92b31ddde486615548ba0c59a8af34b00f546680 100644 (file)
@@ -25,6 +25,7 @@ import org.sonar.server.exceptions.BadRequestException;
 public class Validation {
 
   public static final String CANT_BE_EMPTY_MESSAGE = "errors.cant_be_empty";
+  public static final String IS_TOO_SHORT_MESSAGE = "errors.is_too_short";
   public static final String IS_TOO_LONG_MESSAGE = "errors.is_too_long";
   public static final String IS_ALREADY_USED_MESSAGE = "errors.is_already_used";
 
diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/UserCreatorTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/UserCreatorTest.java
new file mode 100644 (file)
index 0000000..2b7ab1a
--- /dev/null
@@ -0,0 +1,431 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.user;
+
+import com.google.common.base.Strings;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.config.Settings;
+import org.sonar.api.platform.NewUserHandler;
+import org.sonar.api.utils.System2;
+import org.sonar.core.persistence.DbSession;
+import org.sonar.core.persistence.DbTester;
+import org.sonar.core.user.GroupDto;
+import org.sonar.core.user.GroupMembershipDao;
+import org.sonar.core.user.GroupMembershipQuery;
+import org.sonar.core.user.UserDto;
+import org.sonar.server.db.DbClient;
+import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.exceptions.Message;
+import org.sonar.server.user.db.GroupDao;
+import org.sonar.server.user.db.UserDao;
+import org.sonar.server.user.db.UserGroupDao;
+import org.sonar.server.util.Validation;
+import org.sonar.test.DbTests;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static org.fest.assertions.Assertions.assertThat;
+import static org.fest.assertions.Fail.fail;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@Category(DbTests.class)
+@RunWith(MockitoJUnitRunner.class)
+public class UserCreatorTest {
+
+  @Rule
+  public DbTester db = new DbTester();
+
+  @Mock
+  System2 system2;
+
+  @Mock
+  NewUserNotifier newUserNotifier;
+
+  @Captor
+  ArgumentCaptor<NewUserHandler.Context> newUserHandler;
+
+  Settings settings;
+  UserDao userDao;
+  GroupDao groupDao;
+  GroupMembershipFinder groupMembershipFinder;
+  DbSession session;
+
+  UserCreator userCreator;
+
+  @Before
+  public void setUp() throws Exception {
+    settings = new Settings();
+    session = db.myBatis().openSession(false);
+    userDao = new UserDao(db.myBatis(), system2);
+    groupDao = new GroupDao(system2);
+    UserGroupDao userGroupDao = new UserGroupDao();
+    GroupMembershipDao groupMembershipDao = new GroupMembershipDao(db.myBatis());
+    groupMembershipFinder = new GroupMembershipFinder(userDao, groupMembershipDao);
+
+    userCreator = new UserCreator(newUserNotifier, settings, userGroupDao, new DbClient(db.database(), db.myBatis(), userDao, groupDao), system2);
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    session.close();
+  }
+
+  @Test
+  public void create_user() throws Exception {
+    when(system2.now()).thenReturn(1418215735482L);
+    createDefaultGroup();
+
+    userCreator.create(NewUser.create()
+      .setLogin("user")
+      .setName("User")
+      .setEmail("user@mail.com")
+      .setPassword("password")
+      .setPasswordConfirmation("password")
+      .setScmAccounts(newArrayList("u1", "u_1")));
+
+    UserDto dto = userDao.selectNullableByLogin(session, "user");
+    assertThat(dto.getId()).isNotNull();
+    assertThat(dto.getLogin()).isEqualTo("user");
+    assertThat(dto.getName()).isEqualTo("User");
+    assertThat(dto.getEmail()).isEqualTo("user@mail.com");
+    assertThat(dto.getScmAccounts()).contains("u1,u_1");
+    assertThat(dto.isActive()).isTrue();
+
+    assertThat(dto.getSalt()).isNotNull();
+    assertThat(dto.getCryptedPassword()).isNotNull();
+    assertThat(dto.getCreatedAt()).isEqualTo(1418215735482L);
+    assertThat(dto.getUpdatedAt()).isEqualTo(1418215735482L);
+  }
+
+  @Test
+  public void fail_to_create_user_if_login_already_exists() throws Exception {
+    db.prepareDbUnit(getClass(), "fail_to_create_user_if_already_exists.xml");
+
+    try {
+      userCreator.create(NewUser.create()
+        .setLogin("marius")
+        .setName("Marius")
+        .setEmail("marius@mail.com")
+        .setPassword("password")
+        .setPasswordConfirmation("password"));
+      fail();
+    } catch (Exception e) {
+      assertThat(e).isInstanceOf(IllegalArgumentException.class).hasMessage("A user with the login 'marius' already exists");
+    }
+  }
+
+  @Test
+  public void fail_to_create_user_if_login_already_exists_but_inactive() throws Exception {
+    db.prepareDbUnit(getClass(), "fail_to_create_user_if_already_exists_but_inactive.xml");
+
+    try {
+      userCreator.create(NewUser.create()
+        .setLogin("marius")
+        .setName("Marius")
+        .setEmail("marius@mail.com")
+        .setPassword("password")
+        .setPasswordConfirmation("password")
+        .setPreventReactivation(true));
+      fail();
+    } catch (Exception e) {
+      assertThat(e).isInstanceOf(ReactivationException.class).hasMessage("A disabled user with the login 'marius' already exists");
+    }
+  }
+
+  @Test
+  public void fail_to_create_user_with_missing_login() throws Exception {
+    try {
+      userCreator.create(NewUser.create()
+        .setLogin(null)
+        .setName("Marius")
+        .setEmail("marius@mail.com")
+        .setPassword("password")
+        .setPasswordConfirmation("password"));
+      fail();
+    } catch (BadRequestException e) {
+      assertThat(e.errors().messages()).containsOnly(Message.of(Validation.CANT_BE_EMPTY_MESSAGE, "Login"));
+    }
+  }
+
+  @Test
+  public void fail_to_create_user_with_invalid_login() throws Exception {
+    try {
+      userCreator.create(NewUser.create()
+        .setLogin("/marius/")
+        .setName("Marius")
+        .setEmail("marius@mail.com")
+        .setPassword("password")
+        .setPasswordConfirmation("password"));
+      fail();
+    } catch (BadRequestException e) {
+      assertThat(e.errors().messages()).containsOnly(Message.of("user.bad_login"));
+    }
+  }
+
+  @Test
+  public void fail_to_create_user_with_too_short_login() throws Exception {
+    try {
+      userCreator.create(NewUser.create()
+        .setLogin("ma")
+        .setName("Marius")
+        .setEmail("marius@mail.com")
+        .setPassword("password")
+        .setPasswordConfirmation("password"));
+      fail();
+    } catch (BadRequestException e) {
+      assertThat(e.errors().messages()).containsOnly(Message.of(Validation.IS_TOO_SHORT_MESSAGE, "Login", 2));
+    }
+  }
+
+  @Test
+  public void fail_to_create_user_with_too_long_login() throws Exception {
+    try {
+      userCreator.create(NewUser.create()
+        .setLogin(Strings.repeat("m", 256))
+        .setName("Marius")
+        .setEmail("marius@mail.com")
+        .setPassword("password")
+        .setPasswordConfirmation("password"));
+      fail();
+    } catch (BadRequestException e) {
+      assertThat(e.errors().messages()).containsOnly(Message.of(Validation.IS_TOO_LONG_MESSAGE, "Login", 255));
+    }
+  }
+
+  @Test
+  public void fail_to_create_user_with_missing_name() throws Exception {
+    try {
+      userCreator.create(NewUser.create()
+        .setLogin("marius")
+        .setName(null)
+        .setEmail("marius@mail.com")
+        .setPassword("password")
+        .setPasswordConfirmation("password"));
+      fail();
+    } catch (BadRequestException e) {
+      assertThat(e.errors().messages()).containsOnly(Message.of(Validation.CANT_BE_EMPTY_MESSAGE, "Name"));
+    }
+  }
+
+  @Test
+  public void fail_to_create_user_with_too_long_name() throws Exception {
+    try {
+      userCreator.create(NewUser.create()
+        .setLogin("marius")
+        .setName(Strings.repeat("m", 201))
+        .setEmail("marius@mail.com")
+        .setPassword("password")
+        .setPasswordConfirmation("password"));
+      fail();
+    } catch (BadRequestException e) {
+      assertThat(e.errors().messages()).containsOnly(Message.of(Validation.IS_TOO_LONG_MESSAGE, "Name", 200));
+    }
+  }
+
+  @Test
+  public void fail_to_create_user_with_too_long_email() throws Exception {
+    try {
+      userCreator.create(NewUser.create()
+        .setLogin("marius")
+        .setName("Marius")
+        .setEmail(Strings.repeat("m", 101))
+        .setPassword("password")
+        .setPasswordConfirmation("password"));
+      fail();
+    } catch (BadRequestException e) {
+      assertThat(e.errors().messages()).containsOnly(Message.of(Validation.IS_TOO_LONG_MESSAGE, "Email", 100));
+    }
+  }
+
+  @Test
+  public void fail_to_create_user_with_missing_password() throws Exception {
+    try {
+      userCreator.create(NewUser.create()
+        .setLogin("marius")
+        .setName("Marius")
+        .setEmail("marius@mail.com")
+        .setPassword(null)
+        .setPasswordConfirmation("password"));
+      fail();
+    } catch (BadRequestException e) {
+      assertThat(e.errors().messages()).containsOnly(Message.of(Validation.CANT_BE_EMPTY_MESSAGE, "Password"));
+    }
+  }
+
+  @Test
+  public void fail_to_create_user_with_missing_password_confirmation() throws Exception {
+    try {
+      userCreator.create(NewUser.create()
+        .setLogin("marius")
+        .setName("Marius")
+        .setEmail("marius@mail.com")
+        .setPassword("password")
+        .setPasswordConfirmation(null));
+      fail();
+    } catch (BadRequestException e) {
+      assertThat(e.errors().messages()).containsOnly(Message.of(Validation.CANT_BE_EMPTY_MESSAGE, "Password confirmation"));
+    }
+  }
+
+  @Test
+  public void fail_to_create_user_with_password_not_matching_password_confirmation() throws Exception {
+    try {
+      userCreator.create(NewUser.create()
+        .setLogin("marius")
+        .setName("Marius")
+        .setEmail("marius@mail.com")
+        .setPassword("password")
+        .setPasswordConfirmation("password2"));
+      fail();
+    } catch (BadRequestException e) {
+      assertThat(e.errors().messages()).containsOnly(Message.of("user.password_doesnt_match_confirmation"));
+    }
+  }
+
+  @Test
+  public void fail_to_create_user_with_many_errors() throws Exception {
+    try {
+      userCreator.create(NewUser.create()
+        .setLogin("")
+        .setName("")
+        .setEmail("marius@mail.com")
+        .setPassword("")
+        .setPasswordConfirmation(""));
+      fail();
+    } catch (BadRequestException e) {
+      assertThat(e.errors().messages()).hasSize(4);
+    }
+  }
+
+  @Test
+  public void notify_new_user() throws Exception {
+    createDefaultGroup();
+
+    userCreator.create(NewUser.create()
+      .setLogin("user")
+      .setName("User")
+      .setEmail("user@mail.com")
+      .setPassword("password")
+      .setPasswordConfirmation("password")
+      .setScmAccounts(newArrayList("u1", "u_1")));
+
+    verify(newUserNotifier).onNewUser(newUserHandler.capture());
+    assertThat(newUserHandler.getValue().getLogin()).isEqualTo("user");
+    assertThat(newUserHandler.getValue().getName()).isEqualTo("User");
+    assertThat(newUserHandler.getValue().getEmail()).isEqualTo("user@mail.com");
+  }
+
+  @Test
+  public void associate_default_groups_when_creating_user() throws Exception {
+    createDefaultGroup();
+
+    userCreator.create(NewUser.create()
+      .setLogin("user")
+      .setName("User")
+      .setEmail("user@mail.com")
+      .setPassword("password")
+      .setPasswordConfirmation("password")
+      .setScmAccounts(newArrayList("u1", "u_1")));
+
+    GroupMembershipFinder.Membership membership = groupMembershipFinder.find(GroupMembershipQuery.builder().login("user").build());
+    assertThat(membership.groups()).hasSize(1);
+    assertThat(membership.groups().get(0).name()).isEqualTo("sonar-users");
+    assertThat(membership.groups().get(0).isMember()).isTrue();
+  }
+
+  @Test
+  public void fail_to_associate_default_groups_to_user_if_no_default_group() throws Exception {
+    settings.setProperty(CoreProperties.CORE_DEFAULT_GROUP, (String) null);
+
+    try {
+      userCreator.create(NewUser.create()
+        .setLogin("user")
+        .setName("User")
+        .setEmail("user@mail.com")
+        .setPassword("password")
+        .setPasswordConfirmation("password")
+        .setScmAccounts(newArrayList("u1", "u_1")));
+    } catch (Exception e) {
+      assertThat(e).isInstanceOf(IllegalStateException.class).hasMessage("The default group property 'sonar.defaultGroup' is null");
+    }
+  }
+
+  @Test
+  public void reactivate_user() throws Exception {
+    db.prepareDbUnit(getClass(), "reactivate_user.xml");
+    when(system2.now()).thenReturn(1418215735486L);
+    createDefaultGroup();
+
+    userCreator.create(NewUser.create()
+      .setLogin("marius")
+      .setName("Marius2")
+      .setEmail("marius2@mail.com")
+      .setPassword("password2")
+      .setPasswordConfirmation("password2")
+      .setPreventReactivation(false));
+
+    UserDto dto = userDao.selectNullableByLogin(session, "marius");
+    assertThat(dto.isActive()).isTrue();
+    assertThat(dto.getName()).isEqualTo("Marius");
+    assertThat(dto.getEmail()).isEqualTo("marius@lesbronzes.fr");
+    assertThat(dto.getScmAccounts()).contains("ma,marius33");
+
+    assertThat(dto.getSalt()).isEqualTo("79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365");
+    assertThat(dto.getCryptedPassword()).isEqualTo("650d2261c98361e2f67f90ce5c65a95e7d8ea2fg");
+    assertThat(dto.getCreatedAt()).isEqualTo(1418215735482L);
+    assertThat(dto.getUpdatedAt()).isEqualTo(1418215735486L);
+  }
+
+  @Test
+  public void associate_default_groups_when_reactivating_user() throws Exception {
+    db.prepareDbUnit(getClass(), "associate_default_groups_when_reactivating_user.xml");
+    createDefaultGroup();
+
+    userCreator.create(NewUser.create()
+      .setLogin("marius")
+      .setName("Marius2")
+      .setEmail("marius2@mail.com")
+      .setPassword("password2")
+      .setPasswordConfirmation("password2")
+      .setPreventReactivation(false));
+
+    GroupMembershipFinder.Membership membership = groupMembershipFinder.find(GroupMembershipQuery.builder().login("marius").groupSearch("sonar-users").build());
+    assertThat(membership.groups()).hasSize(1);
+    assertThat(membership.groups().get(0).name()).isEqualTo("sonar-users");
+    assertThat(membership.groups().get(0).isMember()).isTrue();
+  }
+
+  private void createDefaultGroup() {
+    settings.setProperty(CoreProperties.CORE_DEFAULT_GROUP, "sonar-users");
+    groupDao.insert(session, new GroupDto().setName("sonar-users").setDescription("Sonar Users"));
+    session.commit();
+  }
+}
index 0503b1aabfb3ab1604f5724072a49ec840b5b57c..c7a61cc6d92e0336e9f557b55df7543cc5efa281 100644 (file)
@@ -24,14 +24,19 @@ import org.junit.After;
 import org.junit.Before;
 import org.junit.ClassRule;
 import org.junit.Test;
+import org.sonar.api.CoreProperties;
+import org.sonar.core.permission.GlobalPermissions;
 import org.sonar.core.persistence.DbSession;
+import org.sonar.core.user.GroupDto;
 import org.sonar.core.user.UserDto;
 import org.sonar.server.db.DbClient;
 import org.sonar.server.es.EsClient;
+import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.tester.ServerTester;
+import org.sonar.server.user.db.UserDao;
 import org.sonar.server.user.index.UserIndexDefinition;
-import org.sonar.server.user.index.UserIndexer;
 
+import static com.google.common.collect.Lists.newArrayList;
 import static org.fest.assertions.Assertions.assertThat;
 
 public class UserServiceMediumTest {
@@ -43,7 +48,7 @@ public class UserServiceMediumTest {
   DbClient dbClient;
   DbSession session;
 
-  UserIndexer indexer;
+  UserService service;
 
   @Before
   public void setUp() throws Exception {
@@ -51,7 +56,7 @@ public class UserServiceMediumTest {
     dbClient = tester.get(DbClient.class);
     esClient = tester.get(EsClient.class);
     session = dbClient.openSession(false);
-    indexer = tester.get(UserIndexer.class);
+    service = tester.get(UserService.class);
   }
 
   @After
@@ -59,6 +64,38 @@ public class UserServiceMediumTest {
     session.close();
   }
 
+  @Test
+  public void create_user() throws Exception {
+    MockUserSession.set().setLogin("john").setGlobalPermissions(GlobalPermissions.SYSTEM_ADMIN);
+    GroupDto userGroup = new GroupDto().setName(CoreProperties.CORE_DEFAULT_GROUP_DEFAULT_VALUE);
+    dbClient.groupDao().insert(session, userGroup);
+    session.commit();
+
+    service.create(NewUser.create()
+      .setLogin("user")
+      .setName("User")
+      .setEmail("user@mail.com")
+      .setPassword("password")
+      .setPasswordConfirmation("password")
+      .setScmAccounts(newArrayList("u1", "u_1")));
+
+    assertThat(tester.get(UserDao.class).selectNullableByLogin(session, "user")).isNotNull();
+    assertThat(esClient.prepareGet(UserIndexDefinition.INDEX, UserIndexDefinition.TYPE_USER, "user").get().isExists()).isTrue();
+  }
+
+  @Test(expected = ForbiddenException.class)
+  public void fail_to_create_user_without_sys_admin_permission() throws Exception {
+    MockUserSession.set().setLogin("john").setGlobalPermissions(GlobalPermissions.DASHBOARD_SHARING);
+
+    service.create(NewUser.create()
+      .setLogin("user")
+      .setName("User")
+      .setEmail("user@mail.com")
+      .setPassword("password")
+      .setPasswordConfirmation("password")
+      .setScmAccounts(newArrayList("u1", "u_1")));
+  }
+
   @Test
   public void index() throws Exception {
     UserDto userDto = new UserDto().setLogin("user").setEmail("user@mail.com").setCreatedAt(System.currentTimeMillis()).setUpdatedAt(System.currentTimeMillis());
@@ -66,7 +103,7 @@ public class UserServiceMediumTest {
     session.commit();
     assertThat(esClient.prepareGet(UserIndexDefinition.INDEX, UserIndexDefinition.TYPE_USER, "user").get().isExists()).isFalse();
 
-    indexer.index();
+    service.index();
     assertThat(esClient.prepareGet(UserIndexDefinition.INDEX, UserIndexDefinition.TYPE_USER, "user").get().isExists()).isTrue();
   }
 
diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/db/UserDaoTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/db/UserDaoTest.java
new file mode 100644 (file)
index 0000000..dcb7ca9
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.user.db;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.utils.System2;
+import org.sonar.core.persistence.DbSession;
+import org.sonar.core.persistence.DbTester;
+import org.sonar.core.user.UserDto;
+import org.sonar.server.exceptions.NotFoundException;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.fest.assertions.Fail.fail;
+import static org.mockito.Mockito.mock;
+
+public class UserDaoTest {
+
+  @Rule
+  public DbTester db = new DbTester();
+
+  private UserDao dao;
+  private DbSession session;
+
+  @Before
+  public void before() throws Exception {
+    this.session = db.myBatis().openSession(false);
+    System2 system2 = mock(System2.class);
+    this.dao = new UserDao(db.myBatis(), system2);
+  }
+
+  @After
+  public void after() {
+    this.session.close();
+  }
+
+  @Test
+  public void select_by_login() {
+    db.prepareDbUnit(getClass(), "select_by_login.xml");
+
+    UserDto dto = dao.selectByLogin(session, "marius");
+    assertThat(dto.getId()).isEqualTo(101);
+    assertThat(dto.getLogin()).isEqualTo("marius");
+    assertThat(dto.getName()).isEqualTo("Marius");
+    assertThat(dto.getEmail()).isEqualTo("marius@lesbronzes.fr");
+    assertThat(dto.isActive()).isTrue();
+    assertThat(dto.getScmAccounts()).isEqualTo("ma,marius33");
+    assertThat(dto.getSalt()).isEqualTo("79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365");
+    assertThat(dto.getCryptedPassword()).isEqualTo("650d2261c98361e2f67f90ce5c65a95e7d8ea2fg");
+    assertThat(dto.getCreatedAt()).isEqualTo(1418215735482L);
+    assertThat(dto.getUpdatedAt()).isEqualTo(1418215735485L);
+  }
+
+  @Test
+  public void select_by_login_with_unknown_login() {
+    try {
+      dao.selectByLogin(session, "unknown");
+      fail();
+    } catch (Exception e) {
+      assertThat(e).isInstanceOf(NotFoundException.class).hasMessage("User with login 'unknown' has not been found");
+    }
+  }
+
+  @Test
+  public void select_nullable_by_login() {
+    db.prepareDbUnit(getClass(), "select_by_login.xml");
+
+    assertThat(dao.selectNullableByLogin(session, "marius")).isNotNull();
+
+    assertThat(dao.selectNullableByLogin(session, "unknown")).isNull();
+  }
+
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/db/UserGroupDaoTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/db/UserGroupDaoTest.java
new file mode 100644 (file)
index 0000000..6c898f6
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.user.db;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.core.persistence.DbSession;
+import org.sonar.core.persistence.DbTester;
+import org.sonar.core.user.UserGroupDto;
+
+public class UserGroupDaoTest {
+
+  @Rule
+  public DbTester db = new DbTester();
+
+  private UserGroupDao dao;
+  private DbSession session;
+
+  @Before
+  public void before() throws Exception {
+    this.session = db.myBatis().openSession(false);
+    this.dao = new UserGroupDao();
+  }
+
+  @After
+  public void after() {
+    this.session.close();
+  }
+
+  @Test
+  public void insert() {
+    UserGroupDto userGroupDto = new UserGroupDto().setUserId(1L).setGroupId(2L);
+    dao.insert(session, userGroupDto);
+    session.commit();
+
+    db.assertDbUnit(getClass(), "insert-result.xml", "groups_users");
+  }
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/user/UserCreatorTest/associate_default_groups_when_reactivating_user.xml b/server/sonar-server/src/test/resources/org/sonar/server/user/UserCreatorTest/associate_default_groups_when_reactivating_user.xml
new file mode 100644 (file)
index 0000000..d4b4767
--- /dev/null
@@ -0,0 +1,10 @@
+<dataset>
+
+  <users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[false]" scm_accounts="ma,marius33" created_at="1418215735482" updated_at="1418215735485"
+         salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
+
+  <groups id="1" name="sonar-devs" description="Sonar Devs" created_at="2014-09-08" updated_at="2014-09-08"/>
+
+  <groups_users user_id="1" group_id="1"/>
+
+</dataset>
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/user/UserCreatorTest/fail_to_create_user_if_already_exists.xml b/server/sonar-server/src/test/resources/org/sonar/server/user/UserCreatorTest/fail_to_create_user_if_already_exists.xml
new file mode 100644 (file)
index 0000000..3c22b1e
--- /dev/null
@@ -0,0 +1,6 @@
+<dataset>
+
+  <users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[true]" scm_accounts="ma,marius33" created_at="1418215735482" updated_at="1418215735485"
+         salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
+
+</dataset>
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/user/UserCreatorTest/fail_to_create_user_if_already_exists_but_inactive.xml b/server/sonar-server/src/test/resources/org/sonar/server/user/UserCreatorTest/fail_to_create_user_if_already_exists_but_inactive.xml
new file mode 100644 (file)
index 0000000..24fd96b
--- /dev/null
@@ -0,0 +1,6 @@
+<dataset>
+
+  <users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[false]" scm_accounts="ma,marius33" created_at="1418215735482" updated_at="1418215735485"
+         salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
+
+</dataset>
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/user/UserCreatorTest/reactivate_user.xml b/server/sonar-server/src/test/resources/org/sonar/server/user/UserCreatorTest/reactivate_user.xml
new file mode 100644 (file)
index 0000000..24fd96b
--- /dev/null
@@ -0,0 +1,6 @@
+<dataset>
+
+  <users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[false]" scm_accounts="ma,marius33" created_at="1418215735482" updated_at="1418215735485"
+         salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
+
+</dataset>
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/user/db/UserDaoTest/select_by_login.xml b/server/sonar-server/src/test/resources/org/sonar/server/user/db/UserDaoTest/select_by_login.xml
new file mode 100644 (file)
index 0000000..0b24553
--- /dev/null
@@ -0,0 +1,8 @@
+<dataset>
+
+  <users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[true]" scm_accounts="ma,marius33" created_at="1418215735482" updated_at="1418215735485"
+         salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
+  <users id="102" login="sbrandhof" name="Simon Brandhof" email="marius@lesbronzes.fr" active="[true]" scm_accounts="[null]" created_at="1418215735482" updated_at="1418215735485"
+         salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8366" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fh"/>
+
+</dataset>
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/user/db/UserGroupDaoTest/insert-result.xml b/server/sonar-server/src/test/resources/org/sonar/server/user/db/UserGroupDaoTest/insert-result.xml
new file mode 100644 (file)
index 0000000..2e5bb1a
--- /dev/null
@@ -0,0 +1,5 @@
+<dataset>
+
+  <groups_users user_id="1" group_id="2"/>
+
+</dataset>
index 56c4d53cb1a9e8c75b0b8b40198b81fab2919837..6b7bc5504b57e596bb7afd1df873bc99981a088a 100644 (file)
@@ -52,7 +52,6 @@ class UsersController < ApplicationController
         render :text => 'ok', :status => 200
       else
         # case user: don't exist, WITH ERRORS when create
-        # case user: exist and ACTIVE, whith or without errors when create
         @user = user
         user.errors.full_messages.each { |msg| @errors<<msg }
         render :partial => 'users/create_form', :status => 400
index 7785eba83c8e5c2517c0ffe186c47ff4477108ce..ea38fbd742f50ecc1b7327b54032ba1eb06c3a45 100644 (file)
@@ -154,6 +154,7 @@ public class MyBatis implements BatchComponent, ServerComponent {
     loadAlias(conf, "SchemaMigration", SchemaMigrationDto.class);
     loadAlias(conf, "User", UserDto.class);
     loadAlias(conf, "UserRole", UserRoleDto.class);
+    loadAlias(conf, "UserGroup", UserGroupDto.class);
     loadAlias(conf, "Widget", WidgetDto.class);
     loadAlias(conf, "WidgetProperty", WidgetPropertyDto.class);
     loadAlias(conf, "MeasureModel", MeasureModel.class);
@@ -191,7 +192,7 @@ public class MyBatis implements BatchComponent, ServerComponent {
       IssueMapper.class, IssueChangeMapper.class, IssueFilterMapper.class, IssueFilterFavouriteMapper.class,
       LoadedTemplateMapper.class, MeasureFilterMapper.class, Migration44Mapper.class, PermissionTemplateMapper.class, PropertiesMapper.class, PurgeMapper.class,
       ResourceKeyUpdaterMapper.class, ResourceIndexerMapper.class, ResourceSnapshotMapper.class, RoleMapper.class, RuleMapper.class,
-      SchemaMigrationMapper.class, SemaphoreMapper.class, UserMapper.class, GroupMapper.class, WidgetMapper.class, WidgetPropertyMapper.class,
+      SchemaMigrationMapper.class, SemaphoreMapper.class, UserMapper.class, GroupMapper.class, UserGroupMapper.class, WidgetMapper.class, WidgetPropertyMapper.class,
       org.sonar.api.database.model.MeasureMapper.class, SnapshotDataMapper.class, FileSourceMapper.class, ActionPlanMapper.class,
       ActionPlanStatsMapper.class,
       NotificationQueueMapper.class, CharacteristicMapper.class,
index 0f0ab61adb1b770f469831049a12febc869818e7..0cff129eb8a6de4a50a702e3d3339dc94f059f39 100644 (file)
@@ -117,6 +117,11 @@ public class UserDao implements BatchComponent, ServerComponent, DaoComponent {
     return dto;
   }
 
+  public UserDto update(SqlSession session, UserDto dto) {
+    session.getMapper(UserMapper.class).update(dto);
+    return dto;
+  }
+
   /**
    * Deactivate a user and drops all his preferences.
    * @return false if the user does not exist, true if the existing user has been deactivated
@@ -152,6 +157,8 @@ public class UserDao implements BatchComponent, ServerComponent, DaoComponent {
    * Search for group by name.
    *
    * @return the group, null if group not found
+   *
+   * TODO should be moved to GroupDao
    */
   @CheckForNull
   public GroupDto selectGroupByName(String name, DbSession session) {
@@ -159,6 +166,9 @@ public class UserDao implements BatchComponent, ServerComponent, DaoComponent {
     return mapper.selectGroupByName(name);
   }
 
+  /**
+   * TODO should be moved to GroupDao
+   */
   @CheckForNull
   public GroupDto selectGroupByName(String name) {
     DbSession session = mybatis.openSession(false);
index 4fc269fc0fa41cac464a1dba5e66deefd3e43fe6..14a174a8d98ec3c1cb59937398b0ab0f979f82f5 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonar.core.user;
 
+import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 
 /**
@@ -29,9 +30,12 @@ public class UserDto {
   private String login;
   private String name;
   private String email;
+  private boolean active = true;
+  private String scmAccounts;
+  private String cryptedPassword;
+  private String salt;
   private Long createdAt;
   private Long updatedAt;
-  private boolean active = true;
 
   public Long getId() {
     return id;
@@ -60,6 +64,7 @@ public class UserDto {
     return this;
   }
 
+  @CheckForNull
   public String getEmail() {
     return email;
   }
@@ -69,6 +74,42 @@ public class UserDto {
     return this;
   }
 
+  public boolean isActive() {
+    return active;
+  }
+
+  public UserDto setActive(boolean b) {
+    this.active = b;
+    return this;
+  }
+
+  public String getScmAccounts() {
+    return scmAccounts;
+  }
+
+  public UserDto setScmAccounts(String scmAccounts) {
+    this.scmAccounts = scmAccounts;
+    return this;
+  }
+
+  public String getCryptedPassword() {
+    return cryptedPassword;
+  }
+
+  public UserDto setCryptedPassword(String cryptedPassword) {
+    this.cryptedPassword = cryptedPassword;
+    return this;
+  }
+
+  public String getSalt() {
+    return salt;
+  }
+
+  public UserDto setSalt(String salt) {
+    this.salt = salt;
+    return this;
+  }
+
   public Long getCreatedAt() {
     return createdAt;
   }
@@ -87,15 +128,6 @@ public class UserDto {
     return this;
   }
 
-  public boolean isActive() {
-    return active;
-  }
-
-  public UserDto setActive(boolean b) {
-    this.active = b;
-    return this;
-  }
-
   public DefaultUser toUser() {
     return new DefaultUser()
       .setLogin(login)
diff --git a/sonar-core/src/main/java/org/sonar/core/user/UserGroupDto.java b/sonar-core/src/main/java/org/sonar/core/user/UserGroupDto.java
new file mode 100644 (file)
index 0000000..99d300c
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.core.user;
+
+public class UserGroupDto {
+
+  private Long userId;
+  private Long groupId;
+
+  public Long getUserId() {
+    return userId;
+  }
+
+  public UserGroupDto setUserId(Long userId) {
+    this.userId = userId;
+    return this;
+  }
+
+  public Long getGroupId() {
+    return groupId;
+  }
+
+  public UserGroupDto setGroupId(Long groupId) {
+    this.groupId = groupId;
+    return this;
+  }
+}
diff --git a/sonar-core/src/main/java/org/sonar/core/user/UserGroupMapper.java b/sonar-core/src/main/java/org/sonar/core/user/UserGroupMapper.java
new file mode 100644 (file)
index 0000000..328abc4
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.core.user;
+
+public interface UserGroupMapper {
+
+  void insert(UserGroupDto userGroupDto);
+
+}
index 4e18104c46d6a35f74a21bdb073a49e880f0e490..329fd09beeb58e4d4a21a89a41bd1ff5375f9f35 100644 (file)
@@ -28,6 +28,9 @@ import java.util.List;
 
 public interface UserMapper {
 
+  @CheckForNull
+  UserDto selectByLogin(String login);
+
   @CheckForNull
   UserDto selectUser(long userId);
 
@@ -46,6 +49,8 @@ public interface UserMapper {
 
   void insert(UserDto userDto);
 
+  void update(UserDto userDto);
+
   void removeUserFromGroups(long userId);
 
   void deleteUserActiveDashboards(long userId);
diff --git a/sonar-core/src/main/resources/org/sonar/core/user/UserGroupMapper.xml b/sonar-core/src/main/resources/org/sonar/core/user/UserGroupMapper.xml
new file mode 100644 (file)
index 0000000..ce48eea
--- /dev/null
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+
+<mapper namespace="org.sonar.core.user.UserGroupMapper">
+
+  <insert id="insert" parameterType="UserGroup" useGeneratedKeys="false">
+    INSERT INTO groups_users (user_id, group_id)
+    VALUES (#{userId}, #{groupId})
+  </insert>
+
+</mapper>
index 69a559d0002356104f146d65c4478d20ea71dd06..588c91f0c1d1862c85113a55491d5ca6a72bbf85 100644 (file)
@@ -8,12 +8,21 @@
     u.login as login,
     u.name as name,
     u.email as email,
+    u.active as "active",
+    u.scm_accounts as "scmAccounts",
+    u.salt as "salt",
+    u.crypted_password as "cryptedPassword",
     u.created_at as "createdAt",
-    u.updated_at as "updatedAt",
-    u.active as "active"
+    u.updated_at as "updatedAt"
   </sql>
 
-    <select id="selectUser" parameterType="long" resultType="User">
+  <select id="selectByLogin" parameterType="String" resultType="User">
+    SELECT
+    <include refid="userColumns"/>
+    FROM users u WHERE u.login=#{login}
+  </select>
+
+  <select id="selectUser" parameterType="long" resultType="User">
     SELECT
     <include refid="userColumns"/>
     FROM users u WHERE u.id=#{id}
@@ -30,8 +39,8 @@
     <include refid="userColumns"/>
     FROM users u WHERE
     (<foreach item="login" index="index" collection="logins" open="(" separator=" or " close=")">
-      u.login=#{login}
-    </foreach>)
+    u.login=#{login}
+  </foreach>)
   </select>
 
   <select id="selectUsers" parameterType="map" resultType="User">
@@ -55,7 +64,6 @@
     ORDER BY u.name
   </select>
 
-
   <select id="selectGroupByName" parameterType="string" resultType="Group">
     SELECT id, name, description, created_at AS "createdAt", updated_at AS "updatedAt"
     FROM groups WHERE name=#{id}
   </update>
 
   <insert id="insert" parameterType="User" keyColumn="id" useGeneratedKeys="true" keyProperty="id">
-    INSERT INTO users (login, name, email, active, created_at, updated_at)
-    VALUES (#{login}, #{name}, #{email}, ${_true}, #{createdAt}, #{updatedAt})
+    INSERT INTO users (login, name, email, active, scm_accounts, salt, crypted_password, created_at, updated_at)
+    VALUES (#{login}, #{name}, #{email}, #{active}, #{scmAccounts}, #{salt}, #{cryptedPassword}, #{createdAt}, #{updatedAt})
+  </insert>
+
+  <insert id="update" parameterType="User">
+    UPDATE users set name=#{name}, email=#{email}, active=#{active}, scm_accounts=#{scmAccounts}, salt=#{salt}, crypted_password=#{cryptedPassword}, updated_at=#{updatedAt}
+    WHERE login = #{login}
   </insert>
 
 </mapper>
index 19317e9aecfead43a092402767bafdad2de9df43..51171c7d7b332b1d68631eb7130c7e4b74e57e39 100644 (file)
@@ -1959,6 +1959,14 @@ alerts.operator.!\==is not
 events.add_an_event=Add an event
 events.name_required=Name (required)
 
+#------------------------------------------------------------------------------
+#
+# USER
+#
+#------------------------------------------------------------------------------
+user.bad_login=Use only letters, numbers, and .-_@ please.
+user.password_doesnt_match_confirmation=Password doesn't match confirmation.
+
 
 #------------------------------------------------------------------------------
 #
index f0def854ee1cbc850ecf46a3723d3929c7278418..1e19d64f40c221126a6673d7e80d258154102763 100644 (file)
@@ -88,7 +88,7 @@ public class UserDaoTest extends AbstractDaoTestCase {
   @Test
   public void selectUsersByLogins_empty_logins() throws Exception {
     // no need to access db
-    Collection<UserDto> users = dao.selectUsersByLogins(Collections.<String> emptyList());
+    Collection<UserDto> users = dao.selectUsersByLogins(Collections.<String>emptyList());
     assertThat(users).isEmpty();
   }
 
@@ -177,7 +177,17 @@ public class UserDaoTest extends AbstractDaoTestCase {
   public void insert_user() {
     Long date = DateUtils.parseDate("2014-06-20").getTime();
 
-    UserDto userDto = new UserDto().setId(1L).setLogin("john").setName("John").setEmail("jo@hn.com").setCreatedAt(date).setUpdatedAt(date);
+    UserDto userDto = new UserDto()
+      .setId(1L)
+      .setLogin("john")
+      .setName("John")
+      .setEmail("jo@hn.com")
+      .setScmAccounts("jo.hn,john2")
+      .setActive(true)
+      .setSalt("1234")
+      .setCryptedPassword("abcd")
+      .setCreatedAt(date)
+      .setUpdatedAt(date);
     dao.insert(session, userDto);
     session.commit();
 
@@ -187,10 +197,45 @@ public class UserDaoTest extends AbstractDaoTestCase {
     assertThat(user.getLogin()).isEqualTo("john");
     assertThat(user.getName()).isEqualTo("John");
     assertThat(user.getEmail()).isEqualTo("jo@hn.com");
+    assertThat(user.isActive()).isTrue();
+    assertThat(user.getScmAccounts()).isEqualTo("jo.hn,john2");
+    assertThat(user.getSalt()).isEqualTo("1234");
+    assertThat(user.getCryptedPassword()).isEqualTo("abcd");
     assertThat(user.getCreatedAt()).isEqualTo(date);
     assertThat(user.getUpdatedAt()).isEqualTo(date);
+  }
+
+  @Test
+  public void update_user() {
+    setupData("update_user");
+
+    Long date = DateUtils.parseDate("2014-06-21").getTime();
+
+    UserDto userDto = new UserDto()
+      .setId(1L)
+      .setLogin("john")
+      .setName("John Doo")
+      .setEmail("jodoo@hn.com")
+      .setScmAccounts("jo.hn,john2,johndoo")
+      .setActive(false)
+      .setSalt("12345")
+      .setCryptedPassword("abcde")
+      .setUpdatedAt(date);
+    dao.update(session, userDto);
+    session.commit();
 
-    // checkTables("insert", new String[] {"id"}, "users");
+    UserDto user = dao.getUser(1);
+    assertThat(user).isNotNull();
+    assertThat(user.getId()).isEqualTo(1L);
+    assertThat(user.getLogin()).isEqualTo("john");
+    assertThat(user.getName()).isEqualTo("John Doo");
+    assertThat(user.getEmail()).isEqualTo("jodoo@hn.com");
+    assertThat(user.isActive()).isFalse();
+    assertThat(user.getScmAccounts()).isEqualTo("jo.hn,john2,johndoo");
+    assertThat(user.getSalt()).isEqualTo("12345");
+    assertThat(user.getCryptedPassword()).isEqualTo("abcde");
+    assertThat(user.getCreatedAt()).isEqualTo(1418215735482L);
+    assertThat(user.getUpdatedAt()).isEqualTo(date);
   }
 
   @Test
diff --git a/sonar-core/src/test/resources/org/sonar/core/user/UserDaoTest/update_user.xml b/sonar-core/src/test/resources/org/sonar/core/user/UserDaoTest/update_user.xml
new file mode 100644 (file)
index 0000000..f5b642f
--- /dev/null
@@ -0,0 +1,3 @@
+<dataset>
+  <users id="1" login="john" name="John" email="jo@hn.com" created_at="1418215735482" updated_at="1418215735482" active="[true]"/>
+</dataset>