]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6226 Add ext identity columns in USERS table
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Tue, 19 Jan 2016 10:48:05 +0000 (11:48 +0100)
committerJulien Lancelot <julien.lancelot@sonarsource.com>
Mon, 25 Jan 2016 14:26:49 +0000 (15:26 +0100)
38 files changed:
server/sonar-server/src/main/java/org/sonar/server/user/NewUser.java
server/sonar-server/src/main/java/org/sonar/server/user/UserUpdater.java
server/sonar-server/src/test/java/org/sonar/server/user/NewUserTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/user/UserUpdaterTest.java
server/sonar-server/src/test/java/org/sonar/server/user/ws/CreateActionTest.java
server/sonar-server/src/test/resources/org/sonar/server/user/UserUpdaterTest/associate_default_groups_when_reactivating_user.xml
server/sonar-server/src/test/resources/org/sonar/server/user/UserUpdaterTest/associate_default_groups_when_updating_user.xml
server/sonar-server/src/test/resources/org/sonar/server/user/UserUpdaterTest/fail_to_create_user_when_scm_account_is_already_used.xml
server/sonar-server/src/test/resources/org/sonar/server/user/UserUpdaterTest/fail_to_create_user_when_scm_account_is_already_used_by_many_user.xml
server/sonar-server/src/test/resources/org/sonar/server/user/UserUpdaterTest/fail_to_reactivate_user_if_not_disabled.xml
server/sonar-server/src/test/resources/org/sonar/server/user/UserUpdaterTest/fail_to_update_user_when_scm_account_is_already_used.xml
server/sonar-server/src/test/resources/org/sonar/server/user/UserUpdaterTest/not_associate_default_group_when_updating_user_if_already_existing.xml
server/sonar-server/src/test/resources/org/sonar/server/user/UserUpdaterTest/reactivate_user.xml
server/sonar-server/src/test/resources/org/sonar/server/user/UserUpdaterTest/update_technical_user.xml
server/sonar-server/src/test/resources/org/sonar/server/user/UserUpdaterTest/update_user.xml
server/sonar-web/src/main/webapp/WEB-INF/db/migrate/1011_add_users_identity_columns.rb [new file with mode: 0644]
server/sonar-web/src/main/webapp/WEB-INF/db/migrate/1012_migrate_users_identity.rb [new file with mode: 0644]
server/sonar-web/src/main/webapp/WEB-INF/db/migrate/1013_add_unique_index_on_users_identity_columns.rb [new file with mode: 0644]
sonar-db/src/main/java/org/sonar/db/user/UserDao.java
sonar-db/src/main/java/org/sonar/db/user/UserDto.java
sonar-db/src/main/java/org/sonar/db/user/UserMapper.java
sonar-db/src/main/java/org/sonar/db/version/DatabaseVersion.java
sonar-db/src/main/java/org/sonar/db/version/MigrationStepModule.java
sonar-db/src/main/java/org/sonar/db/version/v54/AddUsersIdentityColumns.java [new file with mode: 0644]
sonar-db/src/main/java/org/sonar/db/version/v54/MigrateUsersIdentity.java [new file with mode: 0644]
sonar-db/src/main/resources/org/sonar/db/user/UserMapper.xml
sonar-db/src/main/resources/org/sonar/db/version/rows-h2.sql
sonar-db/src/main/resources/org/sonar/db/version/schema-h2.ddl
sonar-db/src/test/java/org/sonar/db/user/UserDaoTest.java
sonar-db/src/test/java/org/sonar/db/user/UserTesting.java
sonar-db/src/test/java/org/sonar/db/version/MigrationStepModuleTest.java
sonar-db/src/test/java/org/sonar/db/version/v54/AddUsersIdentityColumnsTest.java [new file with mode: 0644]
sonar-db/src/test/java/org/sonar/db/version/v54/MigrateUsersIdentityTest.java [new file with mode: 0644]
sonar-db/src/test/resources/org/sonar/db/user/UserDaoTest/select_users_by_ext_identity.xml [new file with mode: 0644]
sonar-db/src/test/resources/org/sonar/db/version/v54/AddUsersIdentityColumnsTest/schema.sql [new file with mode: 0644]
sonar-db/src/test/resources/org/sonar/db/version/v54/MigrateUsersIdentityTest/migrate-result.xml [new file with mode: 0644]
sonar-db/src/test/resources/org/sonar/db/version/v54/MigrateUsersIdentityTest/migrate.xml [new file with mode: 0644]
sonar-db/src/test/resources/org/sonar/db/version/v54/MigrateUsersIdentityTest/schema.sql [new file with mode: 0644]

index ea50902199390cb6c2bd37c387ff82d369c30c24..01d49b93f9deff1eca399f3816f51b86b67882fc 100644 (file)
@@ -23,15 +23,16 @@ import java.util.List;
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 
+import static java.util.Objects.requireNonNull;
+
 public class NewUser {
 
   private String login;
   private String name;
   private String email;
   private List<String> scmAccounts;
-
   private String password;
-  private String passwordConfirmation;
+  private ExternalIdentity externalIdentity;
 
   private NewUser() {
     // No direct call to this constructor
@@ -82,12 +83,40 @@ public class NewUser {
     return password;
   }
 
-  public NewUser setPassword(String password) {
+  public NewUser setPassword(@Nullable String password) {
     this.password = password;
     return this;
   }
 
+  @Nullable
+  public ExternalIdentity externalIdentity() {
+    return externalIdentity;
+  }
+
+  public NewUser setExternalIdentity(@Nullable ExternalIdentity externalIdentity) {
+    this.externalIdentity = externalIdentity;
+    return this;
+  }
+
   public static NewUser create() {
     return new NewUser();
   }
+
+  public static class ExternalIdentity {
+    private String provider;
+    private String id;
+
+    public ExternalIdentity(String provider, String id) {
+      this.provider = requireNonNull(provider, "Identity provider cannot be null");
+      this.id = requireNonNull(id, "Identity id cannot be null");
+    }
+
+    public String getProvider() {
+      return provider;
+    }
+
+    public String getId() {
+      return id;
+    }
+  }
 }
index b61fe46b76e5bacf752e95d7de32f0991bb6e045..2003643daee2afe1fb034e29f9b403d0f2a565a7 100644 (file)
@@ -20,6 +20,7 @@
 package org.sonar.server.user;
 
 import com.google.common.base.Joiner;
+import com.google.common.base.Optional;
 import com.google.common.base.Predicate;
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
@@ -55,6 +56,8 @@ import static org.sonar.api.CoreProperties.CORE_AUTHENTICATOR_LOCAL_USERS;
 @ServerSide
 public class UserUpdater {
 
+  private static final String SQ_AUTHORITY = "sonarqube";
+
   private static final String LOGIN_PARAM = "Login";
   private static final String PASSWORD_PARAM = "Password";
   private static final String NAME_PARAM = "Name";
@@ -95,27 +98,16 @@ public class UserUpdater {
     }
   }
 
-  public boolean create(DbSession dbSession, NewUser newUser){
+  public boolean create(DbSession dbSession, NewUser newUser) {
     boolean isUserReactivated = false;
     UserDto userDto = createNewUserDto(dbSession, newUser);
     String login = userDto.getLogin();
-    UserDto existingUser = dbClient.userDao().selectByLogin(dbSession, login);
-    if (existingUser == null) {
+    Optional<UserDto> existingUser = dbClient.userDao().selectByExternalIdentity(dbSession, userDto.getExternalIdentity(), userDto.getExternalIdentityProvider());
+    if (!existingUser.isPresent()) {
       saveUser(dbSession, userDto);
       addDefaultGroup(dbSession, userDto);
     } else {
-      if (existingUser.isActive()) {
-        throw new IllegalArgumentException(String.format("An active user with login '%s' already exists", login));
-      }
-      UpdateUser updateUser = UpdateUser.create(login)
-        .setName(newUser.name())
-        .setEmail(newUser.email())
-        .setScmAccounts(newUser.scmAccounts())
-        .setPassword(newUser.password());
-      updateUserDto(dbSession, updateUser, existingUser);
-      updateUser(dbSession, existingUser);
-      addDefaultGroup(dbSession, existingUser);
-      isUserReactivated = true;
+      isUserReactivated = updateExistingUser(dbSession, existingUser.get(), login, newUser);
     }
     dbSession.commit();
     notifyNewUser(userDto.getLogin(), userDto.getName(), newUser.email());
@@ -123,6 +115,21 @@ public class UserUpdater {
     return isUserReactivated;
   }
 
+  private boolean updateExistingUser(DbSession dbSession, UserDto existingUser, String login, NewUser newUser) {
+    if (existingUser.isActive()) {
+      throw new IllegalArgumentException(String.format("An active user with login '%s' already exists", login));
+    }
+    UpdateUser updateUser = UpdateUser.create(login)
+      .setName(newUser.name())
+      .setEmail(newUser.email())
+      .setScmAccounts(newUser.scmAccounts())
+      .setPassword(newUser.password());
+    updateUserDto(dbSession, updateUser, existingUser);
+    updateUser(dbSession, existingUser);
+    addDefaultGroup(dbSession, existingUser);
+    return true;
+  }
+
   public void update(UpdateUser updateUser) {
     DbSession dbSession = dbClient.openSession(false);
     try {
@@ -194,6 +201,15 @@ public class UserUpdater {
       userDto.setScmAccounts(scmAccounts);
     }
 
+    NewUser.ExternalIdentity externalIdentity = newUser.externalIdentity();
+    if (externalIdentity == null) {
+      userDto.setExternalIdentity(login);
+      userDto.setExternalIdentityProvider(SQ_AUTHORITY);
+    } else {
+      userDto.setExternalIdentity(externalIdentity.getId());
+      userDto.setExternalIdentityProvider(externalIdentity.getProvider());
+    }
+
     if (!messages.isEmpty()) {
       throw new BadRequestException(messages);
     }
@@ -243,7 +259,7 @@ public class UserUpdater {
     }
   }
 
-  private static void validateLoginFormat(@Nullable String login, List<Message> messages) {
+  private static String validateLoginFormat(@Nullable String login, List<Message> messages) {
     checkNotEmptyParam(login, LOGIN_PARAM, messages);
     if (!Strings.isNullOrEmpty(login)) {
       if (login.length() < LOGIN_MIN_LENGTH) {
@@ -254,6 +270,7 @@ public class UserUpdater {
         messages.add(Message.of("user.bad_login"));
       }
     }
+    return login;
   }
 
   private static void validateNameFormat(@Nullable String name, List<Message> messages) {
@@ -280,7 +297,7 @@ public class UserUpdater {
   }
 
   private void validateScmAccounts(DbSession dbSession, List<String> scmAccounts, @Nullable String login, @Nullable String email, @Nullable UserDto existingUser,
-    List<Message> messages) {
+                                   List<Message> messages) {
     for (String scmAccount : scmAccounts) {
       if (scmAccount.equals(login) || scmAccount.equals(email)) {
         messages.add(Message.of("user.login_or_email_used_as_scm_account"));
diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/NewUserTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/NewUserTest.java
new file mode 100644 (file)
index 0000000..4fa85b5
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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 org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class NewUserTest {
+
+  @Rule
+  public ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void create_new_user() throws Exception {
+    NewUser newUser = NewUser.create()
+      .setLogin("login")
+      .setName("name")
+      .setEmail("email")
+      .setPassword("password")
+      .setScmAccounts(asList("login1", "login2"));
+
+    assertThat(newUser.login()).isEqualTo("login");
+    assertThat(newUser.name()).isEqualTo("name");
+    assertThat(newUser.email()).isEqualTo("email");
+    assertThat(newUser.password()).isEqualTo("password");
+    assertThat(newUser.scmAccounts()).contains("login1", "login2");
+    assertThat(newUser.externalIdentity()).isNull();
+  }
+
+  @Test
+  public void create_new_user_with_minimal_fields() throws Exception {
+    NewUser newUser = NewUser.create();
+
+    assertThat(newUser.login()).isNull();
+    assertThat(newUser.name()).isNull();
+    assertThat(newUser.email()).isNull();
+    assertThat(newUser.password()).isNull();
+    assertThat(newUser.scmAccounts()).isNull();
+  }
+
+  @Test
+  public void create_new_user_with_authority() throws Exception {
+    NewUser newUser = NewUser.create()
+      .setLogin("login")
+      .setName("name")
+      .setEmail("email")
+      .setPassword("password")
+      .setExternalIdentity(new NewUser.ExternalIdentity("github", "github_login"));
+
+    assertThat(newUser.externalIdentity().getProvider()).isEqualTo("github");
+    assertThat(newUser.externalIdentity().getId()).isEqualTo("github_login");
+  }
+
+  @Test
+  public void fail_with_NPE_when_identity_provider_is_null() throws Exception {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("Identity provider cannot be null");
+
+    new NewUser.ExternalIdentity(null, "github_login");
+  }
+
+  @Test
+  public void fail_with_NPE_when_identity_id_is_null() throws Exception {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("Identity id cannot be null");
+
+    new NewUser.ExternalIdentity("github", null);
+  }
+}
index c7da098ca41d682ae6a3f1b391db1b6b938a6346..b3f2efc35c0376d1577649ce6ff080f4ca69f65b 100644 (file)
@@ -140,6 +140,35 @@ public class UserUpdaterTest {
         entry("email", "user@mail.com"));
   }
 
+  @Test
+  public void create_user_with_sq_authority_when_no_authority_set() throws Exception {
+    createDefaultGroup();
+
+    userUpdater.create(NewUser.create()
+      .setLogin("user")
+      .setName("User")
+      .setPassword("password"));
+
+    UserDto dto = userDao.selectByLogin(session, "user");
+    assertThat(dto.getExternalIdentity()).isEqualTo("user");
+    assertThat(dto.getExternalIdentityProvider()).isEqualTo("sonarqube");
+  }
+
+  @Test
+  public void create_user_with_authority() {
+    createDefaultGroup();
+
+    userUpdater.create(NewUser.create()
+      .setLogin("ABCD")
+      .setName("User")
+      .setPassword("password")
+      .setExternalIdentity(new NewUser.ExternalIdentity("github", "user")));
+
+    UserDto dto = userDao.selectByLogin(session, "ABCD");
+    assertThat(dto.getExternalIdentity()).isEqualTo("user");
+    assertThat(dto.getExternalIdentityProvider()).isEqualTo("github");
+  }
+
   @Test
   public void create_user_with_minimum_fields() {
     when(system2.now()).thenReturn(1418215735482L);
index 1634b6dbfd9038ffaee267b0cef89254ee0bcd07..6d808226f15ce301d364d174867d683d0fa2bc30 100644 (file)
@@ -33,9 +33,11 @@ import org.sonar.api.utils.System2;
 import org.sonar.core.permission.GlobalPermissions;
 import org.sonar.db.DbSession;
 import org.sonar.db.DbTester;
+import org.sonar.db.user.GroupDao;
 import org.sonar.db.user.GroupDto;
-import org.sonar.db.user.UserDto;
+import org.sonar.db.user.UserDao;
 import org.sonar.db.user.UserGroupDao;
+import org.sonar.db.user.UserTesting;
 import org.sonar.server.db.DbClient;
 import org.sonar.server.es.EsTester;
 import org.sonar.server.exceptions.ForbiddenException;
@@ -43,8 +45,6 @@ import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.user.NewUserNotifier;
 import org.sonar.server.user.SecurityRealmFactory;
 import org.sonar.server.user.UserUpdater;
-import org.sonar.db.user.GroupDao;
-import org.sonar.db.user.UserDao;
 import org.sonar.server.user.index.UserDoc;
 import org.sonar.server.user.index.UserIndex;
 import org.sonar.server.user.index.UserIndexDefinition;
@@ -154,11 +154,7 @@ public class CreateActionTest {
   public void reactivate_user() throws Exception {
     userSessionRule.login("admin").setLocale(Locale.FRENCH).setGlobalPermissions(GlobalPermissions.SYSTEM_ADMIN);
 
-    dbClient.userDao().insert(session, new UserDto()
-      .setEmail("john@email.com")
-      .setLogin("john")
-      .setName("John")
-      .setActive(true));
+    dbClient.userDao().insert(session, UserTesting.newUserDto("john", "John", "john@email.com"));
     session.commit();
     dbClient.userDao().deactivateUserByLogin(session, "john");
     userIndexer.index();
index d0acdb2a121e2183770cadabf47585740f56ffe2..40485114bc3c2dd072b6f7f7f3eccaf0dc1fb85f 100644 (file)
@@ -1,6 +1,8 @@
 <dataset>
 
-  <users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[false]" scm_accounts="&#10;ma&#10;marius33&#10;" created_at="1418215735482" updated_at="1418215735485"
+  <users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[false]" scm_accounts="&#10;ma&#10;marius33&#10;"
+         external_identity_provider="sonarqube" external_identity="marius"
+         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"/>
index 926b75324ad7b9f3000a788f868844ebd6a88787..146d6a2893c5e61a5ad3640fe13de604a1628061 100644 (file)
@@ -1,6 +1,8 @@
 <dataset>
 
-  <users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[true]" scm_accounts="&#10;ma&#10;marius33&#10;" created_at="1418215735482" updated_at="1418215735485"
+  <users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[true]" scm_accounts="&#10;ma&#10;marius33&#10;"
+         external_identity_provider="sonarqube" external_identity="marius"
+         created_at="1418215735482" updated_at="1418215735485"
          salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
 
 </dataset>
index 33dac1a0693643776c18ab9fb7cb33d774baa093..c7ed6aa3f750707323b5e9782d450cad880fd1e7 100644 (file)
@@ -1,6 +1,8 @@
 <dataset>
 
-  <users id="101" login="john" name="John" email="john@email.com" active="[true]" scm_accounts="&#10;jo&#10;" created_at="1418215735482" updated_at="1418215735485"
+  <users id="101" login="john" name="John" email="john@email.com" active="[true]" scm_accounts="&#10;jo&#10;"
+         external_identity_provider="sonarqube" external_identity="john"
+         created_at="1418215735482" updated_at="1418215735485"
          salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
 
 </dataset>
index 1f32fad9c4d75028989ed30fd6c83f1469335d5c..e44742a1dfb2300c522733c345976d4bf01810bf 100644 (file)
@@ -1,10 +1,13 @@
 <dataset>
 
-  <users id="101" login="john" name="John" email="john@email.com" active="[true]" scm_accounts="&#10;jo&#10;" created_at="1418215735482" updated_at="1418215735485"
+  <users id="101" login="john" name="John" email="john@email.com" active="[true]" scm_accounts="&#10;jo&#10;"
+         external_identity_provider="sonarqube" external_identity="john"
+         created_at="1418215735482" updated_at="1418215735485"
          salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
 
-  <users id="102" login="technical-account" name="Technical account" email="john@email.com" active="[true]" scm_accounts="[null]" created_at="1418215735482"
-         updated_at="1418215735485"
+  <users id="102" login="technical-account" name="Technical account" email="john@email.com" active="[true]" scm_accounts="[null]"
+         external_identity_provider="sonarqube" external_identity="technical-account"
+         created_at="1418215735482" updated_at="1418215735485"
          salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
 
 </dataset>
index 926b75324ad7b9f3000a788f868844ebd6a88787..146d6a2893c5e61a5ad3640fe13de604a1628061 100644 (file)
@@ -1,6 +1,8 @@
 <dataset>
 
-  <users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[true]" scm_accounts="&#10;ma&#10;marius33&#10;" created_at="1418215735482" updated_at="1418215735485"
+  <users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[true]" scm_accounts="&#10;ma&#10;marius33&#10;"
+         external_identity_provider="sonarqube" external_identity="marius"
+         created_at="1418215735482" updated_at="1418215735485"
          salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
 
 </dataset>
index 6c8d4d3a854f40b93dd3c43ca4ad95e8d571c374..9704668e7ab335c07a73744a2ca411eabc1b8e7c 100644 (file)
@@ -1,8 +1,12 @@
 <dataset>
 
-  <users id="101" login="john" name="John" email="john@email.com" active="[true]" scm_accounts="&#10;jo&#10;" created_at="1418215735482" updated_at="1418215735485"
+  <users id="101" login="john" name="John" email="john@email.com" active="[true]" scm_accounts="&#10;jo&#10;"
+         external_identity_provider="sonarqube" external_identity="john"
+         created_at="1418215735482" updated_at="1418215735485"
          salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
-  <users id="102" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[true]" scm_accounts="&#10;ma&#10;marius33&#10;" created_at="1418215735482" updated_at="1418215735485"
+  <users id="102" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[true]" scm_accounts="&#10;ma&#10;marius33&#10;"
+         external_identity_provider="sonarqube" external_identity="marius"
+         created_at="1418215735482" updated_at="1418215735485"
          salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
 
 </dataset>
index 8daf3302314131a6d879609a104a38fff84368ed..7b3b2790fee7f7a13a571511aa4aa3c6fd8107ac 100644 (file)
@@ -1,6 +1,8 @@
 <dataset>
 
-  <users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[true]" scm_accounts="&#10;ma&#10;marius33&#10;" created_at="1418215735482" updated_at="1418215735485"
+  <users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[true]" scm_accounts="&#10;ma&#10;marius33&#10;"
+         external_identity_provider="sonarqube" external_identity="marius"
+         created_at="1418215735482" updated_at="1418215735485"
          salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
 
   <groups id="1" name="sonar-users" description="Sonar Users" created_at="2014-09-08" updated_at="2014-09-08"/>
index eef35eda8791ee73cc2c848631f18db9c7bbf27b..372d5dbc4e971f012e96c986c09ee0aaf7cf7ca1 100644 (file)
@@ -1,6 +1,8 @@
 <dataset>
 
-  <users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[false]" scm_accounts="&#10;ma&#10;marius33&#10;" created_at="1418215735482" updated_at="1418215735485"
+  <users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[false]" scm_accounts="&#10;ma&#10;marius33&#10;"
+         external_identity_provider="sonarqube" external_identity="marius"
+         created_at="1418215735482" updated_at="1418215735485"
          salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
 
 </dataset>
index 6b824d7cf252246f439cb8325078a159265eb40f..520f88305602e5ede323b7cadcedea2aa9f56ad7 100644 (file)
@@ -1,6 +1,7 @@
 <dataset>
 
   <users id="101" login="tech_user" name="Tech user" email="tech@user.fr" active="[true]" scm_accounts="[null]"
+         external_identity_provider="sonarqube" external_identity="tech_user"
          created_at="1418215735482" updated_at="1418215735485"
          salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
 
index 926b75324ad7b9f3000a788f868844ebd6a88787..146d6a2893c5e61a5ad3640fe13de604a1628061 100644 (file)
@@ -1,6 +1,8 @@
 <dataset>
 
-  <users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[true]" scm_accounts="&#10;ma&#10;marius33&#10;" created_at="1418215735482" updated_at="1418215735485"
+  <users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[true]" scm_accounts="&#10;ma&#10;marius33&#10;"
+         external_identity_provider="sonarqube" external_identity="marius"
+         created_at="1418215735482" updated_at="1418215735485"
          salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
 
 </dataset>
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/db/migrate/1011_add_users_identity_columns.rb b/server/sonar-web/src/main/webapp/WEB-INF/db/migrate/1011_add_users_identity_columns.rb
new file mode 100644 (file)
index 0000000..fb04528
--- /dev/null
@@ -0,0 +1,31 @@
+#
+# 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.
+#
+
+#
+# SonarQube 5.4
+# SONAR-6226
+#
+class AddUsersIdentityColumns < ActiveRecord::Migration
+
+  def self.up
+    execute_java_migration('org.sonar.db.version.v54.AddUsersIdentityColumns')
+  end
+
+end
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/db/migrate/1012_migrate_users_identity.rb b/server/sonar-web/src/main/webapp/WEB-INF/db/migrate/1012_migrate_users_identity.rb
new file mode 100644 (file)
index 0000000..9ac30e8
--- /dev/null
@@ -0,0 +1,31 @@
+#
+# 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.
+#
+
+#
+# SonarQube 5.4
+# SONAR-6226
+#
+class MigrateUsersIdentity < ActiveRecord::Migration
+
+  def self.up
+    execute_java_migration('org.sonar.db.version.v54.MigrateUsersIdentity')
+  end
+
+end
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/db/migrate/1013_add_unique_index_on_users_identity_columns.rb b/server/sonar-web/src/main/webapp/WEB-INF/db/migrate/1013_add_unique_index_on_users_identity_columns.rb
new file mode 100644 (file)
index 0000000..51fa402
--- /dev/null
@@ -0,0 +1,31 @@
+#
+# 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.
+#
+
+#
+# SonarQube 5.4
+# SONAR-6226
+#
+class AddUniqueIndexOnUsersIdentityColumns < ActiveRecord::Migration
+
+  def self.up
+    add_index :users, [:external_identity, :external_identity_provider], :name => 'uniq_users_identity'
+  end
+
+end
index 8172f465b3de0cbb621b7b0a873de20b3407ef42..8799754185a1261e3213e3ce39ec66f6fceeb3b7 100644 (file)
@@ -20,6 +20,7 @@
 package org.sonar.db.user;
 
 import com.google.common.base.Function;
+import com.google.common.base.Optional;
 import java.util.Collection;
 import java.util.List;
 import javax.annotation.CheckForNull;
@@ -33,6 +34,8 @@ import org.sonar.db.DbSession;
 import org.sonar.db.MyBatis;
 import org.sonar.db.RowNotFoundException;
 
+import static com.google.common.base.Optional.fromNullable;
+
 public class UserDao implements Dao {
 
   private final MyBatis mybatis;
@@ -172,6 +175,18 @@ public class UserDao implements Dao {
     return mapper(session).selectNullableByScmAccountOrLoginOrEmail(scmAccountOrLoginOrEmail, like);
   }
 
+  public Optional<UserDto> selectByExternalIdentity(DbSession session, String extIdentity, String extIdentityProvider){
+    return fromNullable(mapper(session).selectByIdentity(extIdentity, extIdentityProvider));
+  }
+
+  public UserDto selectOrFailByExternalIdentity(DbSession session, String extIdentity, String extIdentityProvider) {
+    Optional<UserDto> user = selectByExternalIdentity(session, extIdentity, extIdentityProvider);
+    if (user.isPresent()) {
+      return user.get();
+    }
+    throw new RowNotFoundException(String.format("User with identity provider '%s' and id '%s' has not been found", extIdentityProvider, extIdentity));
+  }
+
   protected UserMapper mapper(DbSession session) {
     return session.getMapper(UserMapper.class);
   }
index 8c2b067dd728e4477daafc940416bd6f0db5c7b3..a6983bf163833151bbc36867c2eb6800c2cd1d78 100644 (file)
@@ -40,6 +40,8 @@ public class UserDto {
   private String email;
   private boolean active = true;
   private String scmAccounts;
+  private String externalIdentity;
+  private String externalIdentityProvider;
   private String cryptedPassword;
   private String salt;
   private Long createdAt;
@@ -129,6 +131,24 @@ public class UserDto {
     }
   }
 
+  public String getExternalIdentity() {
+    return externalIdentity;
+  }
+
+  public UserDto setExternalIdentity(String authorithy) {
+    this.externalIdentity = authorithy;
+    return this;
+  }
+
+  public String getExternalIdentityProvider() {
+    return externalIdentityProvider;
+  }
+
+  public UserDto setExternalIdentityProvider(String externalIdentityProvider) {
+    this.externalIdentityProvider = externalIdentityProvider;
+    return this;
+  }
+
   public String getCryptedPassword() {
     return cryptedPassword;
   }
index 7835c44bcecedc4883bb3694db16dac5954f6d86..605998588db9a5030f4dda2a2bf5634dd4ddee9f 100644 (file)
@@ -51,6 +51,9 @@ public interface UserMapper {
 
   List<UserDto> selectByLogins(List<String> logins);
 
+  @CheckForNull
+  UserDto selectByIdentity(@Param("extIdentity") String authorityId, @Param("extIdentityProvider") String authorityProvider);
+
   @CheckForNull
   GroupDto selectGroupByName(String name);
 
index b5f895b29abfc03911d7b5d47da37078a6e56bda..43be9647d12529f046563e131f6e79756acad4c0 100644 (file)
@@ -29,7 +29,7 @@ import org.sonar.db.MyBatis;
 
 public class DatabaseVersion {
 
-  public static final int LAST_VERSION = 1010;
+  public static final int LAST_VERSION = 1013;
 
   /**
    * The minimum supported version which can be upgraded. Lower
index d4f25e25adc60bed01d893a3f0bc1648cab336d7..4591717227ecf4a9c8e480e9b88cae1194b72c9f 100644 (file)
@@ -61,7 +61,9 @@ import org.sonar.db.version.v52.RemoveRuleMeasuresOnIssues;
 import org.sonar.db.version.v52.RemoveSnapshotLibraries;
 import org.sonar.db.version.v53.FixMsSqlCollation;
 import org.sonar.db.version.v53.UpdateCustomDashboardInLoadedTemplates;
+import org.sonar.db.version.v54.AddUsersIdentityColumns;
 import org.sonar.db.version.v54.InsertGateAdminPermissionForEachProfileAdmin;
+import org.sonar.db.version.v54.MigrateUsersIdentity;
 import org.sonar.db.version.v54.RemoveComponentPageProperties;
 
 public class MigrationStepModule extends Module {
@@ -121,6 +123,9 @@ public class MigrationStepModule extends Module {
 
       // 5.4
       InsertGateAdminPermissionForEachProfileAdmin.class,
-      RemoveComponentPageProperties.class);
+      RemoveComponentPageProperties.class,
+      AddUsersIdentityColumns.class,
+      MigrateUsersIdentity.class
+    );
   }
 }
diff --git a/sonar-db/src/main/java/org/sonar/db/version/v54/AddUsersIdentityColumns.java b/sonar-db/src/main/java/org/sonar/db/version/v54/AddUsersIdentityColumns.java
new file mode 100644 (file)
index 0000000..989ae5d
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.db.version.v54;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.db.version.AddColumnsBuilder;
+import org.sonar.db.version.DdlChange;
+
+import static org.sonar.db.version.VarcharColumnDef.newVarcharColumnDefBuilder;
+
+/**
+ * Add the following columns to the USERS table :
+ * - external_identity
+ * - external_identity_provider
+ */
+public class AddUsersIdentityColumns extends DdlChange {
+
+  private final Database db;
+
+  public AddUsersIdentityColumns(Database db) {
+    super(db);
+    this.db = db;
+  }
+
+  @Override
+  public void execute(DdlChange.Context context) throws SQLException {
+    context.execute(generateSql());
+  }
+
+  private String generateSql() {
+    return new AddColumnsBuilder(db.getDialect(), "users")
+      .addColumn(newVarcharColumnDefBuilder().setColumnName("external_identity").setLimit(4000).setIsNullable(true).build())
+      .addColumn(newVarcharColumnDefBuilder().setColumnName("external_identity_provider").setLimit(100).setIsNullable(true).build())
+      .build();
+  }
+
+}
diff --git a/sonar-db/src/main/java/org/sonar/db/version/v54/MigrateUsersIdentity.java b/sonar-db/src/main/java/org/sonar/db/version/v54/MigrateUsersIdentity.java
new file mode 100644 (file)
index 0000000..476153f
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.db.version.v54;
+
+import java.sql.SQLException;
+import org.sonar.api.utils.System2;
+import org.sonar.db.Database;
+import org.sonar.db.version.BaseDataChange;
+import org.sonar.db.version.MassUpdate;
+import org.sonar.db.version.Select;
+import org.sonar.db.version.SqlStatement;
+
+/**
+ * Update all users to feed external_identity_provider with 'sonarqube' and external_identity with the login
+ */
+public class MigrateUsersIdentity extends BaseDataChange {
+
+  private final System2 system2;
+
+  public MigrateUsersIdentity(Database db, System2 system2) {
+    super(db);
+    this.system2 = system2;
+  }
+
+  @Override
+  public void execute(Context context) throws SQLException {
+    MassUpdate update = context.prepareMassUpdate().rowPluralName("users");
+    update.select("SELECT u.id, u.login FROM users u");
+    update.update("UPDATE users SET external_identity_provider=?, external_identity=?, updated_at=? WHERE id=? " +
+      "AND external_identity_provider IS NULL AND external_identity IS NULL");
+    update.execute(new MigrationHandler(system2.now()));
+  }
+
+  private static class MigrationHandler implements MassUpdate.Handler {
+    private final long now;
+
+    public MigrationHandler(long now) {
+      this.now = now;
+    }
+
+    @Override
+    public boolean handle(Select.Row row, SqlStatement update) throws SQLException {
+      update.setString(1, "sonarqube");
+      update.setString(2, row.getString(2));
+      update.setLong(3, now);
+      update.setLong(4, row.getLong(1));
+      return true;
+    }
+  }
+}
index 14e57dcd5df284405d865cdab365ed8669168260..a4d3b6364480367eb1e4e7950166544e03aa11ab 100644 (file)
@@ -12,6 +12,8 @@
     u.scm_accounts as "scmAccounts",
     u.salt as "salt",
     u.crypted_password as "cryptedPassword",
+    u.external_identity as "externalIdentity",
+    u.external_identity_provider as "externalIdentityProvider",
     u.created_at as "createdAt",
     u.updated_at as "updatedAt"
   </sql>
     ORDER BY u.name
   </select>
 
+  <select id="selectByIdentity" parameterType="map" resultType="User">
+    SELECT <include refid="userColumns"/>
+    FROM users u
+    <where>
+      u.external_identity=#{extIdentity}
+      AND u.external_identity_provider=#{extIdentityProvider}
+    </where>
+  </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, scm_accounts, salt, crypted_password, created_at, updated_at)
+    INSERT INTO users (login, name, email, active, scm_accounts, external_identity, external_identity_provider, salt, crypted_password, created_at, updated_at)
     VALUES (#{login,jdbcType=VARCHAR}, #{name,jdbcType=VARCHAR}, #{email,jdbcType=VARCHAR}, #{active,jdbcType=BOOLEAN},
-    #{scmAccounts,jdbcType=VARCHAR},
+    #{scmAccounts,jdbcType=VARCHAR}, #{externalIdentity,jdbcType=VARCHAR}, #{externalIdentityProvider,jdbcType=VARCHAR},
     #{salt,jdbcType=VARCHAR}, #{cryptedPassword,jdbcType=VARCHAR}, #{createdAt,jdbcType=BIGINT},
     #{updatedAt,jdbcType=BIGINT})
   </insert>
index c629c8113d2888f72c9c78bd927fa7eebdc9e151..c225c4c782ac190a421fb80e5c09e56b713792c6 100644 (file)
@@ -372,6 +372,9 @@ INSERT INTO SCHEMA_MIGRATIONS(VERSION) VALUES ('1007');
 INSERT INTO SCHEMA_MIGRATIONS(VERSION) VALUES ('1008');
 INSERT INTO SCHEMA_MIGRATIONS(VERSION) VALUES ('1009');
 INSERT INTO SCHEMA_MIGRATIONS(VERSION) VALUES ('1010');
+INSERT INTO SCHEMA_MIGRATIONS(VERSION) VALUES ('1011');
+INSERT INTO SCHEMA_MIGRATIONS(VERSION) VALUES ('1012');
+INSERT INTO SCHEMA_MIGRATIONS(VERSION) VALUES ('1013');
 
 INSERT INTO USERS(ID, LOGIN, NAME, EMAIL, CRYPTED_PASSWORD, SALT, CREATED_AT, UPDATED_AT, REMEMBER_TOKEN, REMEMBER_TOKEN_EXPIRES_AT) VALUES (1, 'admin', 'Administrator', '', 'a373a0e667abb2604c1fd571eb4ad47fe8cc0878', '48bc4b0d93179b5103fd3885ea9119498e9d161b', '1418215735482', '1418215735482', null, null);
 ALTER TABLE USERS ALTER COLUMN ID RESTART WITH 2;
index 0db4c0881af8f9e1ebf90904351532cd75e6e12c..ff5efccc4911839c2be7aeb123d1018daf2395f6 100644 (file)
@@ -320,6 +320,8 @@ CREATE TABLE "USERS" (
   "REMEMBER_TOKEN_EXPIRES_AT" TIMESTAMP,
   "ACTIVE" BOOLEAN DEFAULT TRUE,
   "SCM_ACCOUNTS" VARCHAR(4000),
+  "EXTERNAL_IDENTITY" VARCHAR(4000),
+  "EXTERNAL_IDENTITY_PROVIDER" VARCHAR(100),
   "CREATED_AT" BIGINT,
   "UPDATED_AT" BIGINT
 );
@@ -679,6 +681,8 @@ CREATE INDEX "ISSUE_FILTER_FAVS_USER" ON "ISSUE_FILTER_FAVOURITES" ("USER_LOGIN"
 
 CREATE UNIQUE INDEX "USERS_LOGIN" ON "USERS" ("LOGIN");
 
+CREATE UNIQUE INDEX "UNIQ_USERS_IDENTITY" ON "USERS" ("EXTERNAL_IDENTITY", "EXTERNAL_IDENTITY_PROVIDER");
+
 CREATE INDEX "USERS_UPDATED_AT" ON "USERS" ("UPDATED_AT");
 
 CREATE INDEX "SNAPSHOTS_ROOT_PROJECT_ID" ON "SNAPSHOTS" ("ROOT_PROJECT_ID");
index 69c53d4e6e701e8b1baab8d108e0af3e500b06c1..42b14013f4c559805302b77f980e1d4f98fc0fe0 100644 (file)
@@ -25,6 +25,7 @@ import java.util.List;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.experimental.categories.Category;
+import org.junit.rules.ExpectedException;
 import org.sonar.api.user.UserQuery;
 import org.sonar.api.utils.DateUtils;
 import org.sonar.api.utils.System2;
@@ -35,6 +36,7 @@ import org.sonar.test.DbTests;
 
 import static java.util.Arrays.asList;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.guava.api.Assertions.assertThat;
 import static org.junit.Assert.fail;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -42,6 +44,9 @@ import static org.mockito.Mockito.when;
 @Category(DbTests.class)
 public class UserDaoTest {
 
+  @Rule
+  public ExpectedException thrown = ExpectedException.none();
+
   System2 system2 = mock(System2.class);
 
   @Rule
@@ -160,6 +165,8 @@ public class UserDaoTest {
       .setActive(true)
       .setSalt("1234")
       .setCryptedPassword("abcd")
+      .setExternalIdentity("johngithub")
+      .setExternalIdentityProvider("github")
       .setCreatedAt(date)
       .setUpdatedAt(date);
     underTest.insert(db.getSession(), userDto);
@@ -175,6 +182,8 @@ public class UserDaoTest {
     assertThat(user.getScmAccounts()).isEqualTo(",jo.hn,john2,");
     assertThat(user.getSalt()).isEqualTo("1234");
     assertThat(user.getCryptedPassword()).isEqualTo("abcd");
+    assertThat(user.getExternalIdentity()).isEqualTo("johngithub");
+    assertThat(user.getExternalIdentityProvider()).isEqualTo("github");
     assertThat(user.getCreatedAt()).isEqualTo(date);
     assertThat(user.getUpdatedAt()).isEqualTo(date);
   }
@@ -314,4 +323,23 @@ public class UserDaoTest {
 
     assertThat(underTest.selectByLogin(session, "unknown")).isNull();
   }
+
+  @Test
+  public void select_user_by_external_identity() {
+    db.prepareDbUnit(getClass(), "select_users_by_ext_identity.xml");
+
+    assertThat(underTest.selectByExternalIdentity(session, "mariusgithub", "github")).isPresent();
+    assertThat(underTest.selectByExternalIdentity(session, "mariusgithub", "google")).isAbsent();
+    assertThat(underTest.selectByExternalIdentity(session, "unknown", "unknown")).isAbsent();
+  }
+
+  @Test
+  public void select_or_fail_by_external_identity() throws Exception {
+    db.prepareDbUnit(getClass(), "select_users_by_ext_identity.xml");
+    assertThat(underTest.selectOrFailByExternalIdentity(session, "mariusgithub", "github")).isNotNull();
+
+    thrown.expect(RowNotFoundException.class);
+    thrown.expectMessage("User with identity provider 'unknown' and id 'unknown' has not been found");
+    underTest.selectOrFailByExternalIdentity(session, "unknown", "unknown");
+  }
 }
index 84addbc26e650d0a1e780ac9342b340b8e594352..034e623e756aff902b2a75d1afcb09f943a543d8 100644 (file)
  */
 package org.sonar.db.user;
 
-import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
 import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
 import static org.apache.commons.lang.math.RandomUtils.nextLong;
 
 public class UserTesting {
 
   public static UserDto newUserDto() {
-    UserDto user = new UserDto()
+    return newUserDto(randomAlphanumeric(30), randomAlphanumeric(30), randomAlphanumeric(30));
+  }
+
+  public static UserDto newUserDto(String login, String name, String email) {
+    return new UserDto()
       .setActive(true)
-      .setName(randomAlphanumeric(30))
-      .setEmail(randomAlphabetic(30))
-      .setLogin(randomAlphanumeric(30));
-    user.setCreatedAt(nextLong())
+      .setName(name)
+      .setEmail(email)
+      .setLogin(login)
+      .setExternalIdentity(login)
+      .setExternalIdentityProvider("sonarqube")
+      .setCreatedAt(nextLong())
       .setUpdatedAt(nextLong());
-    return user;
   }
 }
index 4d8c997e9b402e30088a9c4c0118a20617cda64b..4fbf249638939760bce0137cc20a41558ef029db 100644 (file)
@@ -29,6 +29,6 @@ public class MigrationStepModuleTest {
   public void verify_count_of_added_MigrationStep_types() {
     ComponentContainer container = new ComponentContainer();
     new MigrationStepModule().configure(container);
-    assertThat(container.size()).isEqualTo(45);
+    assertThat(container.size()).isEqualTo(47);
   }
 }
diff --git a/sonar-db/src/test/java/org/sonar/db/version/v54/AddUsersIdentityColumnsTest.java b/sonar-db/src/test/java/org/sonar/db/version/v54/AddUsersIdentityColumnsTest.java
new file mode 100644 (file)
index 0000000..4981763
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.db.version.v54;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbTester;
+import org.sonar.db.version.MigrationStep;
+
+import static java.sql.Types.VARCHAR;
+
+public class AddUsersIdentityColumnsTest {
+
+  @Rule
+  public DbTester db = DbTester.createForSchema(System2.INSTANCE, AddUsersIdentityColumnsTest.class, "schema.sql");
+
+  MigrationStep migration;
+
+  @Before
+  public void setUp() {
+    migration = new AddUsersIdentityColumns(db.database());
+  }
+
+  @Test
+  public void update_columns() throws Exception {
+    migration.execute();
+
+    db.assertColumnDefinition("users", "external_identity_provider", VARCHAR, 100);
+    db.assertColumnDefinition("users", "external_identity", VARCHAR, 4000);
+  }
+
+}
diff --git a/sonar-db/src/test/java/org/sonar/db/version/v54/MigrateUsersIdentityTest.java b/sonar-db/src/test/java/org/sonar/db/version/v54/MigrateUsersIdentityTest.java
new file mode 100644 (file)
index 0000000..e6e87c1
--- /dev/null
@@ -0,0 +1,91 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.db.version.v54;
+
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbTester;
+import org.sonar.db.version.MigrationStep;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class MigrateUsersIdentityTest {
+
+  static final String TABLE = "users";
+
+  static final long NOW = 1500000000000L;
+
+  final System2 system2 = mock(System2.class);
+
+  @Rule
+  public DbTester db = DbTester.createForSchema(System2.INSTANCE, MigrateUsersIdentityTest.class, "schema.sql");
+
+  MigrationStep migration;
+
+  @Before
+  public void setUp() {
+    db.executeUpdateSql("truncate table " + TABLE);
+    when(system2.now()).thenReturn(NOW);
+    migration = new MigrateUsersIdentity(db.database(), system2);
+  }
+
+  @Test
+  public void migrate_empty_db() throws Exception {
+    migration.execute();
+  }
+
+  @Test
+  public void migrate() throws Exception {
+    db.prepareDbUnit(this.getClass(), "migrate.xml");
+
+    migration.execute();
+
+    assertThat(db.countRowsOfTable(TABLE)).isEqualTo(2);
+    assertUser(101, "john", NOW);
+    assertUser(102, "arthur", NOW);
+  }
+
+  @Test
+  public void nothing_to_do_on_already_migrated_data() throws Exception {
+    db.prepareDbUnit(this.getClass(), "migrate-result.xml");
+
+    migration.execute();
+
+    assertThat(db.countRowsOfTable(TABLE)).isEqualTo(2);
+    assertUser(101, "john", 1418215735485L);
+    assertUser(102, "arthur", 1418215735485L);
+  }
+
+  private void assertUser(long userId, String expectedAuthorityId, long expectedUpdatedAt) {
+    Map<String, Object> result = db.selectFirst("SELECT u.external_identity as \"externalIdentity\", " +
+      "u.external_identity_provider as \"externalIdentityProvider\", " +
+      "u.updated_at as \"updatedAt\" " +
+      "FROM users u WHERE u.id=" + userId);
+    assertThat(result.get("externalIdentity")).isEqualTo(expectedAuthorityId);
+    assertThat(result.get("externalIdentityProvider")).isEqualTo("sonarqube");
+    assertThat(result.get("updatedAt")).isEqualTo(expectedUpdatedAt);
+  }
+
+}
diff --git a/sonar-db/src/test/resources/org/sonar/db/user/UserDaoTest/select_users_by_ext_identity.xml b/sonar-db/src/test/resources/org/sonar/db/user/UserDaoTest/select_users_by_ext_identity.xml
new file mode 100644 (file)
index 0000000..247f132
--- /dev/null
@@ -0,0 +1,12 @@
+<dataset>
+
+  <users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[true]" scm_accounts="&#10;ma&#10;marius33&#10;"
+         external_identity_provider="github" external_identity="mariusgithub"
+         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]"
+         external_identity_provider="google" external_identity="mariusgoogle"
+         created_at="1418215735482" updated_at="1418215735485"
+         salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8366" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fh"/>
+
+</dataset>
diff --git a/sonar-db/src/test/resources/org/sonar/db/version/v54/AddUsersIdentityColumnsTest/schema.sql b/sonar-db/src/test/resources/org/sonar/db/version/v54/AddUsersIdentityColumnsTest/schema.sql
new file mode 100644 (file)
index 0000000..f2e4e85
--- /dev/null
@@ -0,0 +1,14 @@
+CREATE TABLE "USERS" (
+  "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1),
+  "LOGIN" VARCHAR(255),
+  "NAME" VARCHAR(200),
+  "EMAIL" VARCHAR(100),
+  "CRYPTED_PASSWORD" VARCHAR(40),
+  "SALT" VARCHAR(40),
+  "REMEMBER_TOKEN" VARCHAR(500),
+  "REMEMBER_TOKEN_EXPIRES_AT" TIMESTAMP,
+  "ACTIVE" BOOLEAN DEFAULT TRUE,
+  "SCM_ACCOUNTS" VARCHAR(4000),
+  "CREATED_AT" BIGINT,
+  "UPDATED_AT" BIGINT
+);
diff --git a/sonar-db/src/test/resources/org/sonar/db/version/v54/MigrateUsersIdentityTest/migrate-result.xml b/sonar-db/src/test/resources/org/sonar/db/version/v54/MigrateUsersIdentityTest/migrate-result.xml
new file mode 100644 (file)
index 0000000..7f4a709
--- /dev/null
@@ -0,0 +1,13 @@
+<dataset>
+
+  <users id="101" login="john" name="John" email="john@email.com" active="[true]" scm_accounts="&#10;jo&#10;"
+         external_identity="john" external_identity_provider="sonarqube"
+         created_at="1418215735482" updated_at="1418215735485"
+         salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
+
+  <users id="102" login="arthur" name="Arthur" email="arthur@email.com" active="[false]" scm_accounts=""
+         external_identity="arthur" external_identity_provider="sonarqube"
+         created_at="1418215735482" updated_at="1418215735485"
+         salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
+
+</dataset>
diff --git a/sonar-db/src/test/resources/org/sonar/db/version/v54/MigrateUsersIdentityTest/migrate.xml b/sonar-db/src/test/resources/org/sonar/db/version/v54/MigrateUsersIdentityTest/migrate.xml
new file mode 100644 (file)
index 0000000..0b839d9
--- /dev/null
@@ -0,0 +1,13 @@
+<dataset>
+
+  <users id="101" login="john" name="John" email="john@email.com" active="[true]" scm_accounts="&#10;jo&#10;"
+         external_identity="[null]" external_identity_provider="[null]"
+         created_at="1418215735482" updated_at="1418215735485"
+         salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
+
+  <users id="102" login="arthur" name="Arthur" email="arthur@email.com" active="[false]" scm_accounts=""
+         external_identity="[null]" external_identity_provider="[null]"
+         created_at="1418215735482" updated_at="1418215735485"
+         salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
+
+</dataset>
diff --git a/sonar-db/src/test/resources/org/sonar/db/version/v54/MigrateUsersIdentityTest/schema.sql b/sonar-db/src/test/resources/org/sonar/db/version/v54/MigrateUsersIdentityTest/schema.sql
new file mode 100644 (file)
index 0000000..b9f3610
--- /dev/null
@@ -0,0 +1,16 @@
+CREATE TABLE "USERS" (
+  "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1),
+  "LOGIN" VARCHAR(255),
+  "NAME" VARCHAR(200),
+  "EMAIL" VARCHAR(100),
+  "CRYPTED_PASSWORD" VARCHAR(40),
+  "SALT" VARCHAR(40),
+  "REMEMBER_TOKEN" VARCHAR(500),
+  "REMEMBER_TOKEN_EXPIRES_AT" TIMESTAMP,
+  "ACTIVE" BOOLEAN DEFAULT TRUE,
+  "SCM_ACCOUNTS" VARCHAR(4000),
+  "EXTERNAL_IDENTITY_PROVIDER" VARCHAR(4000),
+  "EXTERNAL_IDENTITY" VARCHAR(100),
+  "CREATED_AT" BIGINT,
+  "UPDATED_AT" BIGINT
+);