]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10598 Display warning page when detecting login update during authentication
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Fri, 4 May 2018 14:18:50 +0000 (16:18 +0200)
committerSonarTech <sonartech@sonarsource.com>
Wed, 23 May 2018 18:20:47 +0000 (20:20 +0200)
* SONAR-10598 Refactor UserIdentityAuthenticator#authenticate to use a ParameterObject
* SONAR-10598 Redirect user when login is updated and update personal org
* SONAR-10598 Improve update of personal organization key
* SONAR-10598 Improve IT stability related to generation of provider ID
* SONAR-10598 Add USERS#ORGANIZATION_UUID
* SONAR-10598 Replace usage of Organizaions#UserId by Users#OrganizationUuid

79 files changed:
server/sonar-db-core/src/main/resources/org/sonar/db/version/schema-h2.ddl
server/sonar-db-dao/src/main/java/org/sonar/db/organization/OrganizationDto.java
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserDto.java
server/sonar-db-dao/src/main/resources/org/sonar/db/organization/OrganizationMapper.xml
server/sonar-db-dao/src/main/resources/org/sonar/db/user/UserMapper.xml
server/sonar-db-dao/src/test/java/org/sonar/db/organization/OrganizationDaoTest.java
server/sonar-db-dao/src/test/java/org/sonar/db/user/UserDaoTest.java
server/sonar-db-dao/src/test/java/org/sonar/db/user/UserTesting.java
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v72/AddOrganizationUuidToUsers.java [new file with mode: 0644]
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v72/DbVersion72.java
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v72/DropUserIdFromOrganizations.java [new file with mode: 0644]
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v72/PopulateOrganizationUuidOnUsers.java [new file with mode: 0644]
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v72/AddExternalIdToUsersTest.java
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v72/AddOrganizationUuidToUsersTest.java [new file with mode: 0644]
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v72/DbVersion72Test.java
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v72/DropUserIdFromOrganizationsTest.java [new file with mode: 0644]
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v72/PopulateOrganizationUuidOnUsersTest.java [new file with mode: 0644]
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v72/AddOrganizationUuidToUsersTest/users.sql [new file with mode: 0644]
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v72/DropUserIdFromOrganizationsTest/organizations.sql [new file with mode: 0644]
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v72/PopulateOrganizationUuidOnUsersTest/schema.sql [new file with mode: 0644]
server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/UserTester.java
server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/Navigation.java
server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/UpdateLoginPage.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationModule.java
server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationRedirection.java
server/sonar-server/src/main/java/org/sonar/server/authentication/BaseContextFactory.java
server/sonar-server/src/main/java/org/sonar/server/authentication/EmailAlreadyExistsException.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/authentication/InitFilter.java
server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2AuthenticationParameters.java
server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2AuthenticationParametersImpl.java
server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2CallbackFilter.java
server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2ContextFactory.java
server/sonar-server/src/main/java/org/sonar/server/authentication/RealmAuthenticator.java
server/sonar-server/src/main/java/org/sonar/server/authentication/SsoAuthenticator.java
server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticator.java
server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticatorImpl.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticatorParameters.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/authentication/exception/EmailAlreadyExistsRedirectionException.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/authentication/exception/RedirectionException.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/authentication/exception/UpdateLoginRedirectionException.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/authentication/exception/package-info.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/organization/OrganizationCreation.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/organization/OrganizationCreationImpl.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/organization/OrganizationUpdater.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/organization/OrganizationUpdaterImpl.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/organization/ws/CreateAction.java
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
server/sonar-server/src/main/java/org/sonar/server/user/UserUpdater.java
server/sonar-server/src/test/java/org/sonar/server/authentication/BaseContextFactoryTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/InitFilterTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2AuthenticationParametersImplTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2CallbackFilterTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2ContextFactoryTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/RealmAuthenticatorTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/SsoAuthenticatorTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/TestUserIdentityAuthenticator.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/authentication/UserIdentityAuthenticatorImplTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/authentication/UserIdentityAuthenticatorTest.java [deleted file]
server/sonar-server/src/test/java/org/sonar/server/organization/OrganizationCreationImplTest.java [deleted file]
server/sonar-server/src/test/java/org/sonar/server/organization/OrganizationUpdaterImplTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/organization/ws/CreateActionTest.java
server/sonar-server/src/test/java/org/sonar/server/user/UserUpdaterCreateTest.java
server/sonar-server/src/test/java/org/sonar/server/user/UserUpdaterReactivateTest.java
server/sonar-server/src/test/java/org/sonar/server/user/UserUpdaterUpdateTest.java
server/sonar-server/src/test/java/org/sonar/server/user/ws/ChangePasswordActionTest.java
server/sonar-server/src/test/java/org/sonar/server/user/ws/CreateActionTest.java
server/sonar-server/src/test/java/org/sonar/server/user/ws/UpdateActionTest.java
server/sonar-web/src/main/js/apps/sessions/components/UpdateLogin.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/sessions/components/__tests__/UpdateLogin-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/UpdateLogin-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/sessions/routes.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties
tests/src/test/java/org/sonarqube/tests/user/BaseIdentityProviderTest.java
tests/src/test/java/org/sonarqube/tests/user/OAuth2IdentityProviderTest.java
tests/src/test/java/org/sonarqube/tests/user/OrganizationBaseIdentityProviderTest.java [new file with mode: 0644]
tests/src/test/java/org/sonarqube/tests/user/OrganizationIdentityProviderTest.java [deleted file]
tests/src/test/java/org/sonarqube/tests/user/SonarCloudOAuth2IdentityProviderTest.java [new file with mode: 0644]
tests/src/test/java/org/sonarqube/tests/user/SonarCloudUpdateLoginDuringAuthenticationTest.java [new file with mode: 0644]
tests/src/test/java/org/sonarqube/tests/user/SonarCloudUserSuite.java

index 896cadc48bb16c164369fb476f2d8645b25ddbc1..4986fd747fa39a5f1f3321a6e778a5cf142c23ca 100644 (file)
@@ -6,7 +6,6 @@ CREATE TABLE "ORGANIZATIONS" (
   "URL" VARCHAR(256),
   "AVATAR_URL" VARCHAR(256),
   "GUARDED" BOOLEAN NOT NULL,
-  "USER_ID" INTEGER,
   "DEFAULT_PERM_TEMPLATE_PROJECT" VARCHAR(40),
   "DEFAULT_PERM_TEMPLATE_VIEW" VARCHAR(40),
   "DEFAULT_GROUP_ID" INTEGER,
@@ -469,10 +468,11 @@ CREATE TABLE "USERS" (
   "IS_ROOT" BOOLEAN NOT NULL,
   "USER_LOCAL" BOOLEAN,
   "ONBOARDED" BOOLEAN NOT NULL,
-  "CREATED_AT" BIGINT,
-  "UPDATED_AT" BIGINT,
   "HOMEPAGE_TYPE" VARCHAR(40),
-  "HOMEPAGE_PARAMETER" VARCHAR(40)
+  "HOMEPAGE_PARAMETER" VARCHAR(40),
+  "ORGANIZATION_UUID" VARCHAR(40),
+  "CREATED_AT" BIGINT,
+  "UPDATED_AT" BIGINT
 );
 CREATE UNIQUE INDEX "USERS_UUID" ON "USERS" ("UUID");
 CREATE UNIQUE INDEX "USERS_LOGIN" ON "USERS" ("LOGIN");
index e8548462f3818b0b82c0edfee71d289762eeca87..b139f04ef4cf72611ba2a16401a5d19747057281 100644 (file)
@@ -111,16 +111,6 @@ public class OrganizationDto {
     return this;
   }
 
-  @CheckForNull
-  public Integer getUserId() {
-    return userId;
-  }
-
-  public OrganizationDto setUserId(@Nullable Integer userId) {
-    this.userId = userId;
-    return this;
-  }
-
   @CheckForNull
   public Integer getDefaultGroupId() {
     return defaultGroupId;
index 129d636730c7cf02f5a58d7ea7f1b3a362b8c041..2c172e374f02a6dd7000a2578fbb5ff03490c300 100644 (file)
@@ -50,13 +50,14 @@ public class UserDto {
   private String salt;
   // Hash method used to generate cryptedPassword, my be null in case of external authentication
   private String hashMethod;
-  private Long createdAt;
-  private Long updatedAt;
   private String homepageType;
   private String homepageParameter;
   private boolean local = true;
   private boolean root = false;
   private boolean onboarded = false;
+  private String organizationUuid;
+  private Long createdAt;
+  private Long updatedAt;
 
   public String getUuid() {
     return uuid;
@@ -218,24 +219,6 @@ public class UserDto {
     return this;
   }
 
-  public Long getCreatedAt() {
-    return createdAt;
-  }
-
-  UserDto setCreatedAt(long createdAt) {
-    this.createdAt = createdAt;
-    return this;
-  }
-
-  public Long getUpdatedAt() {
-    return updatedAt;
-  }
-
-  UserDto setUpdatedAt(long updatedAt) {
-    this.updatedAt = updatedAt;
-    return this;
-  }
-
   @CheckForNull
   public String getHomepageType() {
     return homepageType;
@@ -281,6 +264,34 @@ public class UserDto {
     return this;
   }
 
+  @CheckForNull
+  public String getOrganizationUuid() {
+    return organizationUuid;
+  }
+
+  public UserDto setOrganizationUuid(@Nullable String organizationUuid) {
+    this.organizationUuid = organizationUuid;
+    return this;
+  }
+
+  public Long getCreatedAt() {
+    return createdAt;
+  }
+
+  UserDto setCreatedAt(long createdAt) {
+    this.createdAt = createdAt;
+    return this;
+  }
+
+  public Long getUpdatedAt() {
+    return updatedAt;
+  }
+
+  UserDto setUpdatedAt(long updatedAt) {
+    this.updatedAt = updatedAt;
+    return this;
+  }
+
   public DefaultUser toUser() {
     return new DefaultUser()
       .setLogin(login)
index d48bcec8cfb2c1c0fff83d707376b9af3ae36113..86231226a8f32290505f6b9eacb2bd849c7519e8 100644 (file)
@@ -11,7 +11,6 @@
     org.url as "url",
     org.avatar_url as "avatarUrl",
     org.guarded as "guarded",
-    org.user_id as "userId",
     org.created_at as "createdAt",
     org.updated_at as "updatedAt"
   </sql>
       avatar_url,
       guarded,
       new_project_private,
-      user_id,
       default_quality_gate_uuid,
       created_at,
       updated_at
       #{organization.avatarUrl, jdbcType=VARCHAR},
       #{organization.guarded, jdbcType=BOOLEAN},
       #{newProjectPrivate, jdbcType=BOOLEAN},
-      #{organization.userId, jdbcType=INTEGER},
       #{organization.defaultQualityGateUuid, jdbcType=VARCHAR},
       #{organization.createdAt, jdbcType=BIGINT},
       #{organization.updatedAt, jdbcType=BIGINT}
   <update id="update" parameterType="Organization">
     update organizations
     set
+      kee = #{organization.key, jdbcType=VARCHAR},
       name = #{organization.name, jdbcType=VARCHAR},
       description = #{organization.description, jdbcType=VARCHAR},
       url = #{organization.url, jdbcType=VARCHAR},
index f28b9383fdd03c944b36e0c1ccdcb12c0fcc0e5d..6a37689fbf2471317557f7bba3248b3e4747ae20 100644 (file)
         u.user_local as "local",
         u.is_root as "root",
         u.onboarded as "onboarded",
-        u.created_at as "createdAt",
-        u.updated_at as "updatedAt",
         u.homepage_type as "homepageType",
-        u.homepage_parameter as "homepageParameter"
+        u.homepage_parameter as "homepageParameter",
+        u.organization_uuid as organizationUuid,
+        u.created_at as "createdAt",
+        u.updated_at as "updatedAt"
     </sql>
 
     <select id="selectByUuid" parameterType="String" resultType="User">
         hash_method,
         is_root,
         onboarded,
-        created_at,
-        updated_at,
         homepage_type,
-        homepage_parameter
+        homepage_parameter,
+        organization_uuid,
+        created_at,
+        updated_at
         ) values (
         #{user.uuid,jdbcType=VARCHAR},
         #{user.login,jdbcType=VARCHAR},
         #{user.hashMethod,jdbcType=VARCHAR},
         #{user.root,jdbcType=BOOLEAN},
         #{user.onboarded,jdbcType=BOOLEAN},
-        #{user.createdAt,jdbcType=BIGINT},
-        #{user.updatedAt,jdbcType=BIGINT},
         #{user.homepageType,jdbcType=VARCHAR},
-        #{user.homepageParameter,jdbcType=VARCHAR}
+        #{user.homepageParameter,jdbcType=VARCHAR},
+        #{user.organizationUuid,jdbcType=VARCHAR},
+        #{user.createdAt,jdbcType=BIGINT},
+        #{user.updatedAt,jdbcType=BIGINT}
         )
     </insert>
 
         salt = #{user.salt, jdbcType=VARCHAR},
         crypted_password = #{user.cryptedPassword, jdbcType=BIGINT},
         hash_method = #{user.hashMethod, jdbcType=VARCHAR},
-        updated_at = #{user.updatedAt,jdbcType=BIGINT},
         homepage_type = #{user.homepageType, jdbcType=VARCHAR},
-        homepage_parameter = #{user.homepageParameter, jdbcType=VARCHAR}
+        homepage_parameter = #{user.homepageParameter, jdbcType=VARCHAR},
+        organization_uuid = #{user.organizationUuid, jdbcType=VARCHAR},
+        updated_at = #{user.updatedAt,jdbcType=BIGINT}
         where
         uuid = #{user.uuid, jdbcType=VARCHAR}
     </update>
index 9cafed3cd47a71c9b86662bc3aa5a2c7b56357b1..3c6c238a73fb6d6a4483a9f6250f31998952c465 100644 (file)
@@ -70,8 +70,7 @@ public class OrganizationDaoTest {
     .setUrl("the url 1")
     .setAvatarUrl("the avatar url 1")
     .setGuarded(false)
-    .setDefaultQualityGateUuid("1")
-    .setUserId(1_000);
+    .setDefaultQualityGateUuid("1");
   private static final OrganizationDto ORGANIZATION_DTO_2 = new OrganizationDto()
     .setUuid("uuid 2")
     .setKey("the_key 2")
@@ -80,20 +79,19 @@ public class OrganizationDaoTest {
     .setUrl("the url 2")
     .setAvatarUrl("the avatar url 2")
     .setGuarded(true)
-    .setDefaultQualityGateUuid("1")
-    .setUserId(2_000);
+    .setDefaultQualityGateUuid("1");
   private static final String PERMISSION_1 = "foo";
   private static final String PERMISSION_2 = "bar";
 
   private System2 system2 = mock(System2.class);
 
   @Rule
-  public final DbTester dbTester = DbTester.create(system2).setDisableDefaultOrganization(true);
+  public final DbTester db = DbTester.create(system2).setDisableDefaultOrganization(true);
   @Rule
   public ExpectedException expectedException = ExpectedException.none();
 
-  private DbClient dbClient = dbTester.getDbClient();
-  private DbSession dbSession = dbTester.getSession();
+  private DbClient dbClient = db.getDbClient();
+  private DbSession dbSession = db.getSession();
 
   private OrganizationDao underTest = dbClient.organizationDao();
 
@@ -146,7 +144,7 @@ public class OrganizationDaoTest {
   @Test
   public void description_url_avatarUrl_and_userId_are_optional() {
     when(system2.now()).thenReturn(SOME_DATE);
-    insertOrganization(copyOf(ORGANIZATION_DTO_1).setDescription(null).setUrl(null).setAvatarUrl(null).setUserId(null));
+    insertOrganization(copyOf(ORGANIZATION_DTO_1).setDescription(null).setUrl(null).setAvatarUrl(null));
 
     Map<String, Object> row = selectSingleRow();
     assertThat(row.get("uuid")).isEqualTo(ORGANIZATION_DTO_1.getUuid());
@@ -165,7 +163,7 @@ public class OrganizationDaoTest {
   }
 
   private Object toBool(boolean guarded) {
-    Dialect dialect = dbTester.database().getDialect();
+    Dialect dialect = db.database().getDialect();
     if (dialect.getId().equals(Oracle.ID)) {
       return guarded ? 1L : 0L;
     }
@@ -504,12 +502,12 @@ public class OrganizationDaoTest {
 
   @Test
   public void selectByQuery_filter_on_a_member() {
-    OrganizationDto organization = dbTester.organizations().insert();
-    OrganizationDto anotherOrganization = dbTester.organizations().insert();
-    OrganizationDto organizationWithoutMember = dbTester.organizations().insert();
-    UserDto user = dbTester.users().insertUser();
-    dbTester.organizations().addMember(organization, user);
-    dbTester.organizations().addMember(anotherOrganization, user);
+    OrganizationDto organization = db.organizations().insert();
+    OrganizationDto anotherOrganization = db.organizations().insert();
+    OrganizationDto organizationWithoutMember = db.organizations().insert();
+    UserDto user = db.users().insertUser();
+    db.organizations().addMember(organization, user);
+    db.organizations().addMember(anotherOrganization, user);
 
     List<OrganizationDto> result = underTest.selectByQuery(dbSession, OrganizationQuery.newOrganizationQueryBuilder().setMember(user.getId()).build(), forPage(1).andSize(100));
 
@@ -520,14 +518,14 @@ public class OrganizationDaoTest {
 
   @Test
   public void selectByQuery_filter_on_a_member_and_keys() {
-    OrganizationDto organization = dbTester.organizations().insert();
-    OrganizationDto anotherOrganization = dbTester.organizations().insert();
-    OrganizationDto organizationWithoutKeyProvided = dbTester.organizations().insert();
-    OrganizationDto organizationWithoutMember = dbTester.organizations().insert();
-    UserDto user = dbTester.users().insertUser();
-    dbTester.organizations().addMember(organization, user);
-    dbTester.organizations().addMember(anotherOrganization, user);
-    dbTester.organizations().addMember(organizationWithoutKeyProvided, user);
+    OrganizationDto organization = db.organizations().insert();
+    OrganizationDto anotherOrganization = db.organizations().insert();
+    OrganizationDto organizationWithoutKeyProvided = db.organizations().insert();
+    OrganizationDto organizationWithoutMember = db.organizations().insert();
+    UserDto user = db.users().insertUser();
+    db.organizations().addMember(organization, user);
+    db.organizations().addMember(anotherOrganization, user);
+    db.organizations().addMember(organizationWithoutKeyProvided, user);
 
     List<OrganizationDto> result = underTest.selectByQuery(dbSession, OrganizationQuery.newOrganizationQueryBuilder()
       .setKeys(Arrays.asList(organization.getKey(), anotherOrganization.getKey(), organizationWithoutMember.getKey()))
@@ -669,11 +667,11 @@ public class OrganizationDaoTest {
   @Test
   public void setDefaultQualityGate() {
     when(system2.now()).thenReturn(DATE_3);
-    OrganizationDto organization = dbTester.organizations().insert();
-    QGateWithOrgDto qualityGate = dbTester.qualityGates().insertQualityGate(organization);
+    OrganizationDto organization = db.organizations().insert();
+    QGateWithOrgDto qualityGate = db.qualityGates().insertQualityGate(organization);
 
     underTest.setDefaultQualityGate(dbSession, organization, qualityGate);
-    dbTester.commit();
+    db.commit();
 
     assertThat(dbClient.qualityGateDao().selectDefault(dbSession, organization).getUuid()).isEqualTo(qualityGate.getUuid());
     verifyOrganizationUpdatedAt(organization.getUuid(), DATE_3);
@@ -725,7 +723,7 @@ public class OrganizationDaoTest {
   }
 
   @Test
-  public void update_does_not_update_key_nor_createdAt() {
+  public void update_does_not_update_createdAt() {
     when(system2.now()).thenReturn(DATE_1);
     insertOrganization(ORGANIZATION_DTO_1);
 
@@ -743,7 +741,7 @@ public class OrganizationDaoTest {
 
     Map<String, Object> row = selectSingleRow();
     assertThat(row.get("uuid")).isEqualTo(ORGANIZATION_DTO_1.getUuid());
-    assertThat(row.get("key")).isEqualTo(ORGANIZATION_DTO_1.getKey());
+    assertThat(row.get("key")).isEqualTo("new key");
     assertThat(row.get("name")).isEqualTo("new name");
     assertThat(row.get("description")).isEqualTo("new description");
     assertThat(row.get("url")).isEqualTo("new url");
@@ -781,32 +779,32 @@ public class OrganizationDaoTest {
     String anotherUuid = "uuid";
     insertOrganization(copyOf(ORGANIZATION_DTO_1).setUuid(anotherUuid).setKey("key"));
 
-    assertThat(dbTester.countRowsOfTable("organizations")).isEqualTo(2);
+    assertThat(db.countRowsOfTable("organizations")).isEqualTo(2);
     assertThat(underTest.deleteByUuid(dbSession, anotherUuid)).isEqualTo(1);
     dbSession.commit();
 
     assertThat(underTest.selectByUuid(dbSession, anotherUuid)).isEmpty();
     assertThat(underTest.selectByUuid(dbSession, ORGANIZATION_DTO_1.getUuid())).isNotEmpty();
-    assertThat(dbTester.countRowsOfTable("organizations")).isEqualTo(1);
+    assertThat(db.countRowsOfTable("organizations")).isEqualTo(1);
 
     assertThat(underTest.deleteByUuid(dbSession, anotherUuid)).isEqualTo(0);
     assertThat(underTest.deleteByUuid(dbSession, ORGANIZATION_DTO_1.getUuid())).isEqualTo(1);
     dbSession.commit();
 
     assertThat(underTest.selectByUuid(dbSession, ORGANIZATION_DTO_1.getUuid())).isEmpty();
-    assertThat(dbTester.countRowsOfTable("organizations")).isEqualTo(0);
+    assertThat(db.countRowsOfTable("organizations")).isEqualTo(0);
   }
 
   @Test
   public void selectByPermission_returns_organization_when_user_has_ADMIN_user_permission_on_some_organization() {
-    UserDto user = dbTester.users().insertUser();
-    OrganizationDto organization1 = dbTester.organizations().insert();
-    dbTester.users().insertPermissionOnUser(organization1, user, PERMISSION_2);
-    OrganizationDto organization2 = dbTester.organizations().insert();
-    dbTester.users().insertPermissionOnUser(organization2, user, PERMISSION_2);
-    UserDto otherUser = dbTester.users().insertUser();
-    OrganizationDto organization3 = dbTester.organizations().insert();
-    dbTester.users().insertPermissionOnUser(organization3, otherUser, PERMISSION_2);
+    UserDto user = db.users().insertUser();
+    OrganizationDto organization1 = db.organizations().insert();
+    db.users().insertPermissionOnUser(organization1, user, PERMISSION_2);
+    OrganizationDto organization2 = db.organizations().insert();
+    db.users().insertPermissionOnUser(organization2, user, PERMISSION_2);
+    UserDto otherUser = db.users().insertUser();
+    OrganizationDto organization3 = db.organizations().insert();
+    db.users().insertPermissionOnUser(organization3, otherUser, PERMISSION_2);
 
     assertThat(underTest.selectByPermission(dbSession, user.getId(), PERMISSION_2))
       .extracting(OrganizationDto::getUuid)
@@ -822,20 +820,20 @@ public class OrganizationDaoTest {
 
   @Test
   public void selectByPermission_returns_organization_when_user_has_ADMIN_group_permission_on_some_organization() {
-    UserDto user = dbTester.users().insertUser();
-    OrganizationDto organization1 = dbTester.organizations().insert();
-    GroupDto defaultGroup = dbTester.users().insertGroup(organization1);
-    dbTester.users().insertPermissionOnGroup(defaultGroup, PERMISSION_1);
-    dbTester.users().insertMember(defaultGroup, user);
-    OrganizationDto organization2 = dbTester.organizations().insert();
-    GroupDto group1 = dbTester.users().insertGroup(organization2);
-    dbTester.users().insertPermissionOnGroup(group1, PERMISSION_1);
-    dbTester.users().insertMember(group1, user);
-    UserDto otherUser = dbTester.users().insertUser();
-    OrganizationDto organization3 = dbTester.organizations().insert();
-    GroupDto group2 = dbTester.users().insertGroup(organization3);
-    dbTester.users().insertPermissionOnGroup(group2, PERMISSION_1);
-    dbTester.users().insertMember(group2, otherUser);
+    UserDto user = db.users().insertUser();
+    OrganizationDto organization1 = db.organizations().insert();
+    GroupDto defaultGroup = db.users().insertGroup(organization1);
+    db.users().insertPermissionOnGroup(defaultGroup, PERMISSION_1);
+    db.users().insertMember(defaultGroup, user);
+    OrganizationDto organization2 = db.organizations().insert();
+    GroupDto group1 = db.users().insertGroup(organization2);
+    db.users().insertPermissionOnGroup(group1, PERMISSION_1);
+    db.users().insertMember(group1, user);
+    UserDto otherUser = db.users().insertUser();
+    OrganizationDto organization3 = db.organizations().insert();
+    GroupDto group2 = db.users().insertGroup(organization3);
+    db.users().insertPermissionOnGroup(group2, PERMISSION_1);
+    db.users().insertMember(group2, otherUser);
 
     assertThat(underTest.selectByPermission(dbSession, user.getId(), PERMISSION_1))
       .extracting(OrganizationDto::getUuid)
@@ -852,15 +850,15 @@ public class OrganizationDaoTest {
   @Test
   public void selectByPermission_return_organization_only_once_even_if_user_has_ADMIN_permission_twice_or_more() {
     String permission = "destroy";
-    UserDto user = dbTester.users().insertUser();
-    OrganizationDto organization = dbTester.organizations().insert();
-    GroupDto group1 = dbTester.users().insertGroup(organization);
-    dbTester.users().insertPermissionOnGroup(group1, permission);
-    dbTester.users().insertMember(group1, user);
-    GroupDto group2 = dbTester.users().insertGroup(organization);
-    dbTester.users().insertPermissionOnGroup(group2, permission);
-    dbTester.users().insertMember(group2, user);
-    dbTester.users().insertPermissionOnUser(organization, user, permission);
+    UserDto user = db.users().insertUser();
+    OrganizationDto organization = db.organizations().insert();
+    GroupDto group1 = db.users().insertGroup(organization);
+    db.users().insertPermissionOnGroup(group1, permission);
+    db.users().insertMember(group1, user);
+    GroupDto group2 = db.users().insertGroup(organization);
+    db.users().insertPermissionOnGroup(group2, permission);
+    db.users().insertMember(group2, user);
+    db.users().insertPermissionOnUser(organization, user, permission);
 
     assertThat(underTest.selectByPermission(dbSession, user.getId(), permission))
       .extracting(OrganizationDto::getUuid)
@@ -869,14 +867,14 @@ public class OrganizationDaoTest {
 
   @Test
   public void selectByPermission_returns_organization_only_if_user_has_specific_permission_by_user_permission() {
-    OrganizationDto organization = dbTester.organizations().insert();
-    OrganizationDto otherOrganization = dbTester.organizations().insert();
-    UserDto user = dbTester.users().insertUser();
-    dbTester.users().insertPermissionOnUser(organization, user, PERMISSION_1);
-    dbTester.users().insertPermissionOnUser(otherOrganization, user, PERMISSION_2);
-    UserDto otherUser = dbTester.users().insertUser();
-    dbTester.users().insertPermissionOnUser(organization, otherUser, PERMISSION_2);
-    dbTester.users().insertPermissionOnUser(otherOrganization, otherUser, PERMISSION_1);
+    OrganizationDto organization = db.organizations().insert();
+    OrganizationDto otherOrganization = db.organizations().insert();
+    UserDto user = db.users().insertUser();
+    db.users().insertPermissionOnUser(organization, user, PERMISSION_1);
+    db.users().insertPermissionOnUser(otherOrganization, user, PERMISSION_2);
+    UserDto otherUser = db.users().insertUser();
+    db.users().insertPermissionOnUser(organization, otherUser, PERMISSION_2);
+    db.users().insertPermissionOnUser(otherOrganization, otherUser, PERMISSION_1);
 
     assertThat(underTest.selectByPermission(dbSession, user.getId(), PERMISSION_1))
       .extracting(OrganizationDto::getUuid)
@@ -894,22 +892,22 @@ public class OrganizationDaoTest {
 
   @Test
   public void selectByPermission_returns_organization_only_if_user_has_specific_permission_by_group_permission() {
-    OrganizationDto organization = dbTester.organizations().insert();
-    OrganizationDto otherOrganization = dbTester.organizations().insert();
-    GroupDto group1 = dbTester.users().insertGroup(organization);
-    GroupDto group2 = dbTester.users().insertGroup(organization);
-    GroupDto otherGroup1 = dbTester.users().insertGroup(otherOrganization);
-    GroupDto otherGroup2 = dbTester.users().insertGroup(otherOrganization);
-    dbTester.users().insertPermissionOnGroup(group1, PERMISSION_1);
-    dbTester.users().insertPermissionOnGroup(otherGroup2, PERMISSION_2);
-    dbTester.users().insertPermissionOnGroup(group2, PERMISSION_2);
-    dbTester.users().insertPermissionOnGroup(otherGroup1, PERMISSION_1);
-    UserDto user = dbTester.users().insertUser();
-    dbTester.users().insertMember(group1, user);
-    dbTester.users().insertMember(otherGroup2, user);
-    UserDto otherUser = dbTester.users().insertUser();
-    dbTester.users().insertMember(group2, otherUser);
-    dbTester.users().insertMember(otherGroup1, otherUser);
+    OrganizationDto organization = db.organizations().insert();
+    OrganizationDto otherOrganization = db.organizations().insert();
+    GroupDto group1 = db.users().insertGroup(organization);
+    GroupDto group2 = db.users().insertGroup(organization);
+    GroupDto otherGroup1 = db.users().insertGroup(otherOrganization);
+    GroupDto otherGroup2 = db.users().insertGroup(otherOrganization);
+    db.users().insertPermissionOnGroup(group1, PERMISSION_1);
+    db.users().insertPermissionOnGroup(otherGroup2, PERMISSION_2);
+    db.users().insertPermissionOnGroup(group2, PERMISSION_2);
+    db.users().insertPermissionOnGroup(otherGroup1, PERMISSION_1);
+    UserDto user = db.users().insertUser();
+    db.users().insertMember(group1, user);
+    db.users().insertMember(otherGroup2, user);
+    UserDto otherUser = db.users().insertUser();
+    db.users().insertMember(group2, otherUser);
+    db.users().insertMember(otherGroup1, otherUser);
 
     assertThat(underTest.selectByPermission(dbSession, user.getId(), PERMISSION_1))
       .extracting(OrganizationDto::getUuid)
@@ -936,7 +934,7 @@ public class OrganizationDaoTest {
   }
 
   private void dirtyInsertWithDefaultTemplate(String organizationUuid, @Nullable String project, @Nullable String view) {
-    try (Connection connection = dbTester.database().getDataSource().getConnection();
+    try (Connection connection = db.database().getDataSource().getConnection();
       PreparedStatement preparedStatement = connection.prepareStatement(
         "insert into organizations" +
           "    (" +
@@ -998,7 +996,6 @@ public class OrganizationDaoTest {
     assertThat(dto.getUrl()).isEqualTo(ORGANIZATION_DTO_1.getUrl());
     assertThat(dto.isGuarded()).isEqualTo(ORGANIZATION_DTO_1.isGuarded());
     assertThat(dto.getAvatarUrl()).isEqualTo(ORGANIZATION_DTO_1.getAvatarUrl());
-    assertThat(dto.getUserId()).isEqualTo(ORGANIZATION_DTO_1.getUserId());
     assertThat(dto.getCreatedAt()).isEqualTo(ORGANIZATION_DTO_1.getCreatedAt());
     assertThat(dto.getUpdatedAt()).isEqualTo(ORGANIZATION_DTO_1.getUpdatedAt());
   }
@@ -1010,16 +1007,15 @@ public class OrganizationDaoTest {
     assertThat(dto.getDescription()).isEqualTo(expected.getDescription());
     assertThat(dto.getUrl()).isEqualTo(expected.getUrl());
     assertThat(dto.isGuarded()).isEqualTo(expected.isGuarded());
-    assertThat(dto.getUserId()).isEqualTo(expected.getUserId());
     assertThat(dto.getAvatarUrl()).isEqualTo(expected.getAvatarUrl());
     assertThat(dto.getCreatedAt()).isEqualTo(expected.getCreatedAt());
     assertThat(dto.getUpdatedAt()).isEqualTo(expected.getUpdatedAt());
   }
 
   private Map<String, Object> selectSingleRow() {
-    List<Map<String, Object>> rows = dbTester.select("select" +
+    List<Map<String, Object>> rows = db.select("select" +
       " uuid as \"uuid\", kee as \"key\", name as \"name\",  description as \"description\", url as \"url\", avatar_url as \"avatarUrl\"," +
-      " guarded as \"guarded\", user_id as \"userId\"," +
+      " guarded as \"guarded\"," +
       " created_at as \"createdAt\", updated_at as \"updatedAt\"," +
       " default_perm_template_project as \"projectDefaultPermTemplate\"," +
       " default_perm_template_view as \"viewDefaultPermTemplate\"," +
@@ -1054,7 +1050,7 @@ public class OrganizationDaoTest {
   }
 
   private void verifyOrganizationUpdatedAt(String organization, Long updatedAt) {
-    Map<String, Object> row = dbTester.selectFirst(dbTester.getSession(), String.format("select updated_at as \"updatedAt\" from organizations where uuid='%s'", organization));
+    Map<String, Object> row = db.selectFirst(db.getSession(), String.format("select updated_at as \"updatedAt\" from organizations where uuid='%s'", organization));
     assertThat(row.get("updatedAt")).isEqualTo(updatedAt);
   }
 }
index 2d0f536eaa61946f409114414159f2980ba614e5..7a725cf13be839399536d4664d8b8a4f3247f0c2 100644 (file)
@@ -346,10 +346,11 @@ public class UserDaoTest {
       .setExternalIdentityProvider("github")
       .setExternalId("EXT_ID")
       .setLocal(true)
-      .setCreatedAt(date)
-      .setUpdatedAt(date)
       .setHomepageType("project")
-      .setHomepageParameter("OB1");
+      .setHomepageParameter("OB1")
+      .setOrganizationUuid("ORG_UUID")
+      .setCreatedAt(date)
+      .setUpdatedAt(date);
     underTest.insert(db.getSession(), userDto);
     db.getSession().commit();
 
@@ -372,6 +373,7 @@ public class UserDaoTest {
     assertThat(user.isRoot()).isFalse();
     assertThat(user.getHomepageType()).isEqualTo("project");
     assertThat(user.getHomepageParameter()).isEqualTo("OB1");
+    assertThat(user.getOrganizationUuid()).isEqualTo("ORG_UUID");
   }
 
   @Test
@@ -382,7 +384,8 @@ public class UserDaoTest {
       .setEmail("jo@hn.com")
       .setActive(true)
       .setLocal(true)
-      .setOnboarded(false));
+      .setOnboarded(false)
+      .setOrganizationUuid("OLD_ORG_UUID"));
 
     underTest.update(db.getSession(), newUserDto()
       .setUuid(user.getUuid())
@@ -400,7 +403,8 @@ public class UserDaoTest {
       .setExternalId("EXT_ID")
       .setLocal(false)
       .setHomepageType("project")
-      .setHomepageParameter("OB1"));
+      .setHomepageParameter("OB1")
+      .setOrganizationUuid("ORG_UUID"));
 
     UserDto reloaded = underTest.selectByUuid(db.getSession(), user.getUuid());
     assertThat(reloaded).isNotNull();
@@ -421,6 +425,7 @@ public class UserDaoTest {
     assertThat(reloaded.isRoot()).isFalse();
     assertThat(reloaded.getHomepageType()).isEqualTo("project");
     assertThat(reloaded.getHomepageParameter()).isEqualTo("OB1");
+    assertThat(reloaded.getOrganizationUuid()).isEqualTo("ORG_UUID");
   }
 
   @Test
index debf135c232f68382dde9986b22211c23b9a81f3..37a58e6d43427b47f5d122a1968320ba2c72a3b5 100644 (file)
@@ -44,6 +44,8 @@ public class UserTesting {
       .setExternalIdentityProvider(randomAlphanumeric(40))
       .setSalt(randomAlphanumeric(40))
       .setCryptedPassword(randomAlphanumeric(40))
+      // Default quality gate should be set explicitly when needed in tests
+      .setOrganizationUuid("_NOT_SET_")
       .setCreatedAt(nextLong())
       .setUpdatedAt(nextLong());
   }
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v72/AddOrganizationUuidToUsers.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v72/AddOrganizationUuidToUsers.java
new file mode 100644 (file)
index 0000000..93b6c79
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.platform.db.migration.version.v72;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.UUID_SIZE;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.newVarcharColumnDefBuilder;
+
+public class AddOrganizationUuidToUsers extends DdlChange {
+
+  public AddOrganizationUuidToUsers(Database db) {
+    super(db);
+  }
+
+  @Override
+  public void execute(Context context) throws SQLException {
+    context.execute(new AddColumnsBuilder(getDialect(), "users")
+      .addColumn(newVarcharColumnDefBuilder()
+        .setColumnName("organization_uuid")
+        .setLimit(UUID_SIZE)
+        .setIsNullable(true)
+        .build())
+      .build());
+  }
+
+}
index 997d0407fcf5761a092b6e51e4c040478dd9ffd5..9693526a7fc6dc59b8082df7e3edef1ce7e9f87a 100644 (file)
@@ -42,7 +42,9 @@ public class DbVersion72 implements DbVersion {
       .add(2112, "Populate EXTERNAL_ID on table users", PopulateExternalIdOnUsers.class)
       .add(2113, "Makes same columns of table users not nullable", MakeSomeColumnsOfUsersNotNullable.class)
       .add(2114, "Add unique indexes on table users", AddUniqueIndexesOnUsers.class)
-
+      .add(2115, "Add ORGANIZATION_UUID on table users", AddOrganizationUuidToUsers.class)
+      .add(2116, "Populate ORGANIZATION_UUID in table users", PopulateOrganizationUuidOnUsers.class)
+      .add(2117, "Drop USER_ID from table organizations", DropUserIdFromOrganizations.class)
     ;
   }
 }
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v72/DropUserIdFromOrganizations.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v72/DropUserIdFromOrganizations.java
new file mode 100644 (file)
index 0000000..d2a2233
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.platform.db.migration.version.v72;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.sql.DropColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+public class DropUserIdFromOrganizations extends DdlChange {
+
+  public DropUserIdFromOrganizations(Database db) {
+    super(db);
+  }
+
+  @Override
+  public void execute(Context context) throws SQLException {
+    context.execute(new DropColumnsBuilder(getDialect(), "organizations", "user_id").build());
+  }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v72/PopulateOrganizationUuidOnUsers.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v72/PopulateOrganizationUuidOnUsers.java
new file mode 100644 (file)
index 0000000..2b57c1f
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.platform.db.migration.version.v72;
+
+import java.sql.SQLException;
+import org.sonar.api.utils.System2;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.DataChange;
+import org.sonar.server.platform.db.migration.step.MassUpdate;
+
+public class PopulateOrganizationUuidOnUsers extends DataChange {
+
+  private final System2 system2;
+
+  public PopulateOrganizationUuidOnUsers(Database db, System2 system2) {
+    super(db);
+    this.system2 = system2;
+  }
+
+  @Override
+  public void execute(Context context) throws SQLException {
+    MassUpdate massUpdate = context.prepareMassUpdate().rowPluralName("users");
+    massUpdate.select("SELECT u.id, o.uuid FROM users u " +
+      "INNER JOIN organizations o ON o.user_id=u.id " +
+      "WHERE u.organization_uuid IS NULL");
+    massUpdate.update("UPDATE users SET organization_uuid=?, updated_at=? WHERE id=?");
+
+    long now = system2.now();
+    massUpdate.execute((row, update) -> {
+      long id = row.getLong(1);
+      String organizationUuid = row.getString(2);
+      update.setString(1, organizationUuid);
+      update.setLong(2, now);
+      update.setLong(3, id);
+      return true;
+    });
+  }
+}
index d967c3fc6228109dc58613f2425ee2c7ae8deb3d..2a3b5487eaa76bd1f2dcc161ea822c6fb1035421 100644 (file)
@@ -44,7 +44,7 @@ public class AddExternalIdToUsersTest {
   }
 
   @Test
-  public void migration_is_reentrant() throws SQLException {
+  public void migration_is_not_reentrant() throws SQLException {
     underTest.execute();
 
     expectedException.expect(IllegalStateException.class);
diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v72/AddOrganizationUuidToUsersTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v72/AddOrganizationUuidToUsersTest.java
new file mode 100644 (file)
index 0000000..bc080ca
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.platform.db.migration.version.v72;
+
+import java.sql.SQLException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.db.CoreDbTester;
+
+import static java.sql.Types.VARCHAR;
+
+public class AddOrganizationUuidToUsersTest {
+  @Rule
+  public final CoreDbTester db = CoreDbTester.createForSchema(AddOrganizationUuidToUsersTest.class, "users.sql");
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  private AddOrganizationUuidToUsers underTest = new AddOrganizationUuidToUsers(db.database());
+
+  @Test
+  public void column_is_added_to_table() throws SQLException {
+    underTest.execute();
+
+    db.assertColumnDefinition("users", "organization_uuid", VARCHAR, 40, true);
+  }
+
+  @Test
+  public void migration_is_not_reentrant() throws SQLException {
+    underTest.execute();
+
+    expectedException.expect(IllegalStateException.class);
+
+    underTest.execute();
+  }
+
+}
index 1d4ab0135d7a725e3cc36445b2ccbaf0cd801394..4eef7d6033046170e3ffe1f93922cb3ef973916b 100644 (file)
@@ -34,7 +34,7 @@ public class DbVersion72Test {
 
   @Test
   public void verify_migration_count() {
-    verifyMigrationCount(underTest, 15);
+    verifyMigrationCount(underTest, 18);
   }
 
 }
diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v72/DropUserIdFromOrganizationsTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v72/DropUserIdFromOrganizationsTest.java
new file mode 100644 (file)
index 0000000..1e20fa2
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.platform.db.migration.version.v72;
+
+import java.sql.SQLException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.db.CoreDbTester;
+
+public class DropUserIdFromOrganizationsTest {
+
+  @Rule
+  public final CoreDbTester db = CoreDbTester.createForSchema(DropUserIdFromOrganizationsTest.class, "organizations.sql");
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  private DropUserIdFromOrganizations underTest = new DropUserIdFromOrganizations(db.database());
+
+  @Test
+  public void column_is_removed_from_table() throws SQLException {
+    underTest.execute();
+
+    db.assertColumnDoesNotExist("organizations", "user_id");
+  }
+
+  @Test
+  public void migration_is_not_reentrant() throws SQLException {
+    underTest.execute();
+
+    expectedException.expect(IllegalStateException.class);
+
+    underTest.execute();
+  }
+
+}
diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v72/PopulateOrganizationUuidOnUsersTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v72/PopulateOrganizationUuidOnUsersTest.java
new file mode 100644 (file)
index 0000000..7bb07d5
--- /dev/null
@@ -0,0 +1,131 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.platform.db.migration.version.v72;
+
+import java.sql.SQLException;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+import org.assertj.core.groups.Tuple;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.internal.TestSystem2;
+import org.sonar.db.CoreDbTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+
+public class PopulateOrganizationUuidOnUsersTest {
+
+  private static final long PAST = 5_000_000_000L;
+  private static final long NOW = 10_000_000_000L;
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  @Rule
+  public CoreDbTester db = CoreDbTester.createForSchema(PopulateOrganizationUuidOnUsersTest.class, "schema.sql");
+
+  private System2 system2 = new TestSystem2().setNow(NOW);
+
+  private PopulateOrganizationUuidOnUsers underTest = new PopulateOrganizationUuidOnUsers(db.database(), system2);
+
+  @Test
+  public void update_users() throws SQLException {
+    // User not migrated
+    long userId = insertUser("USER_1", null);
+    insertOrganization("ORG_1", userId);
+    // User already migrated
+    insertOrganization("ORG_2", null);
+    insertUser("USER_2", "ORG_2");
+
+    underTest.execute();
+
+    assertUsers(
+      tuple("USER_1", "ORG_1", NOW),
+      tuple("USER_2", "ORG_2", PAST));
+  }
+
+  @Test
+  public void does_nothing_when_no_personal_organization() throws SQLException {
+    insertUser("USER_1", null);
+    insertUser("USER_2", null);
+
+    underTest.execute();
+
+    assertUsers(
+      tuple("USER_1", null, PAST),
+      tuple("USER_2", null, PAST));
+  }
+
+  @Test
+  public void migration_is_reentrant() throws SQLException {
+    long userId = insertUser("USER_1", null);
+    insertOrganization("ORG_1", userId);
+    insertOrganization("ORG_2", null);
+    insertUser("USER_2", "ORG_2");
+
+    underTest.execute();
+    underTest.execute();
+
+    assertUsers(
+      tuple("USER_1", "ORG_1", NOW),
+      tuple("USER_2", "ORG_2", PAST));
+  }
+
+  private void assertUsers(Tuple... expectedTuples) {
+    assertThat(db.select("SELECT LOGIN, ORGANIZATION_UUID, UPDATED_AT FROM USERS")
+      .stream()
+      .map(map -> new Tuple(map.get("LOGIN"), map.get("ORGANIZATION_UUID"), map.get("UPDATED_AT")))
+      .collect(Collectors.toList()))
+      .containsExactlyInAnyOrder(expectedTuples);
+  }
+
+  private long insertUser(String uuid, @Nullable String organizationUuid) {
+    db.executeInsert("USERS",
+      "ORGANIZATION_UUID", organizationUuid,
+      "UUID", uuid,
+      "LOGIN", uuid,
+      "EXTERNAL_ID", uuid,
+      "EXTERNAL_LOGIN", uuid,
+      "EXTERNAL_IDENTITY_PROVIDER", uuid,
+      "IS_ROOT", false,
+      "ONBOARDED", false,
+      "CREATED_AT", PAST,
+      "UPDATED_AT", PAST);
+    return (long) db.selectFirst(String.format("SELECT ID FROM USERS WHERE uuid='%s'", uuid)).get("ID");
+  }
+
+  private void insertOrganization(String uuid, @Nullable Long userId) {
+    db.executeInsert("ORGANIZATIONS",
+      "UUID", uuid,
+      "USER_ID", userId,
+      "KEE", uuid,
+      "NAME", uuid,
+      "GUARDED", true,
+      "DEFAULT_QUALITY_GATE_UUID", uuid,
+      "NEW_PROJECT_PRIVATE", false,
+      "CREATED_AT", PAST,
+      "UPDATED_AT", PAST);
+  }
+
+}
\ No newline at end of file
diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v72/AddOrganizationUuidToUsersTest/users.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v72/AddOrganizationUuidToUsersTest/users.sql
new file mode 100644 (file)
index 0000000..74cb1f1
--- /dev/null
@@ -0,0 +1,26 @@
+CREATE TABLE "USERS" (
+  "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1),
+  "UUID" VARCHAR(40) NOT NULL,
+  "LOGIN" VARCHAR(255) NOT NULL,
+  "NAME" VARCHAR(200),
+  "EMAIL" VARCHAR(100),
+  "CRYPTED_PASSWORD" VARCHAR(100),
+  "SALT" VARCHAR(40),
+  "HASH_METHOD" VARCHAR(10),
+  "ACTIVE" BOOLEAN DEFAULT TRUE,
+  "SCM_ACCOUNTS" VARCHAR(4000),
+  "EXTERNAL_ID" VARCHAR(255) NOT NULL,
+  "EXTERNAL_LOGIN" VARCHAR(255) NOT NULL,
+  "EXTERNAL_IDENTITY_PROVIDER" VARCHAR(100) NOT NULL,
+  "IS_ROOT" BOOLEAN NOT NULL,
+  "USER_LOCAL" BOOLEAN,
+  "ONBOARDED" BOOLEAN NOT NULL,
+  "CREATED_AT" BIGINT,
+  "UPDATED_AT" BIGINT,
+  "HOMEPAGE_TYPE" VARCHAR(40),
+  "HOMEPAGE_PARAMETER" VARCHAR(40)
+);
+CREATE UNIQUE INDEX "USERS_UUID" ON "USERS" ("UUID");
+CREATE UNIQUE INDEX "USERS_LOGIN" ON "USERS" ("LOGIN");
+CREATE UNIQUE INDEX "UNIQ_EXTERNAL_ID" ON "USERS" ("EXTERNAL_IDENTITY_PROVIDER", "EXTERNAL_ID");
+CREATE INDEX "USERS_UPDATED_AT" ON "USERS" ("UPDATED_AT");
diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v72/DropUserIdFromOrganizationsTest/organizations.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v72/DropUserIdFromOrganizationsTest/organizations.sql
new file mode 100644 (file)
index 0000000..e08101a
--- /dev/null
@@ -0,0 +1,19 @@
+CREATE TABLE "ORGANIZATIONS" (
+  "UUID" VARCHAR(40) NOT NULL PRIMARY KEY,
+  "KEE" VARCHAR(32) NOT NULL,
+  "NAME" VARCHAR(64) NOT NULL,
+  "DESCRIPTION" VARCHAR(256),
+  "URL" VARCHAR(256),
+  "AVATAR_URL" VARCHAR(256),
+  "GUARDED" BOOLEAN NOT NULL,
+  "USER_ID" INTEGER,
+  "DEFAULT_PERM_TEMPLATE_PROJECT" VARCHAR(40),
+  "DEFAULT_PERM_TEMPLATE_VIEW" VARCHAR(40),
+  "DEFAULT_GROUP_ID" INTEGER,
+  "DEFAULT_QUALITY_GATE_UUID" VARCHAR(40) NOT NULL,
+  "NEW_PROJECT_PRIVATE" BOOLEAN NOT NULL,
+  "CREATED_AT" BIGINT NOT NULL,
+  "UPDATED_AT" BIGINT NOT NULL
+);
+CREATE UNIQUE INDEX "PK_ORGANIZATIONS" ON "ORGANIZATIONS" ("UUID");
+CREATE UNIQUE INDEX "ORGANIZATION_KEY" ON "ORGANIZATIONS" ("KEE");
\ No newline at end of file
diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v72/PopulateOrganizationUuidOnUsersTest/schema.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v72/PopulateOrganizationUuidOnUsersTest/schema.sql
new file mode 100644 (file)
index 0000000..194d796
--- /dev/null
@@ -0,0 +1,47 @@
+CREATE TABLE "USERS" (
+  "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1),
+  "UUID" VARCHAR(40) NOT NULL,
+  "LOGIN" VARCHAR(255) NOT NULL,
+  "NAME" VARCHAR(200),
+  "EMAIL" VARCHAR(100),
+  "CRYPTED_PASSWORD" VARCHAR(100),
+  "SALT" VARCHAR(40),
+  "HASH_METHOD" VARCHAR(10),
+  "ACTIVE" BOOLEAN DEFAULT TRUE,
+  "SCM_ACCOUNTS" VARCHAR(4000),
+  "EXTERNAL_ID" VARCHAR(255) NOT NULL,
+  "EXTERNAL_LOGIN" VARCHAR(255) NOT NULL,
+  "EXTERNAL_IDENTITY_PROVIDER" VARCHAR(100) NOT NULL,
+  "IS_ROOT" BOOLEAN NOT NULL,
+  "USER_LOCAL" BOOLEAN,
+  "ONBOARDED" BOOLEAN NOT NULL,
+  "HOMEPAGE_TYPE" VARCHAR(40),
+  "HOMEPAGE_PARAMETER" VARCHAR(40),
+  "ORGANIZATION_UUID" VARCHAR(40),
+  "CREATED_AT" BIGINT,
+  "UPDATED_AT" BIGINT
+);
+CREATE UNIQUE INDEX "USERS_UUID" ON "USERS" ("UUID");
+CREATE UNIQUE INDEX "USERS_LOGIN" ON "USERS" ("LOGIN");
+CREATE UNIQUE INDEX "UNIQ_EXTERNAL_ID" ON "USERS" ("EXTERNAL_IDENTITY_PROVIDER", "EXTERNAL_ID");
+CREATE INDEX "USERS_UPDATED_AT" ON "USERS" ("UPDATED_AT");
+
+CREATE TABLE "ORGANIZATIONS" (
+  "UUID" VARCHAR(40) NOT NULL PRIMARY KEY,
+  "KEE" VARCHAR(32) NOT NULL,
+  "NAME" VARCHAR(64) NOT NULL,
+  "DESCRIPTION" VARCHAR(256),
+  "URL" VARCHAR(256),
+  "AVATAR_URL" VARCHAR(256),
+  "GUARDED" BOOLEAN NOT NULL,
+  "USER_ID" INTEGER,
+  "DEFAULT_PERM_TEMPLATE_PROJECT" VARCHAR(40),
+  "DEFAULT_PERM_TEMPLATE_VIEW" VARCHAR(40),
+  "DEFAULT_GROUP_ID" INTEGER,
+  "DEFAULT_QUALITY_GATE_UUID" VARCHAR(40) NOT NULL,
+  "NEW_PROJECT_PRIVATE" BOOLEAN NOT NULL,
+  "CREATED_AT" BIGINT NOT NULL,
+  "UPDATED_AT" BIGINT NOT NULL
+);
+CREATE UNIQUE INDEX "PK_ORGANIZATIONS" ON "ORGANIZATIONS" ("UUID");
+CREATE UNIQUE INDEX "ORGANIZATION_KEY" ON "ORGANIZATIONS" ("KEE");
\ No newline at end of file
index 5db4d5da72034723d2c9dec782cec1d2517f3229..c911e9f80764cf2394434021a1c7dadb0bb05404 100644 (file)
@@ -110,13 +110,6 @@ public class UserTester {
     return user;
   }
 
-  @SafeVarargs
-  public final User generateMemberOfDefaultOrganization(Consumer<CreateRequest>... populators) {
-    User user = generate(populators);
-    session.wsClient().organizations().addMember(new AddMemberRequest().setOrganization(DEFAULT_ORGANIZATION_KEY).setLogin(user.getLogin()));
-    return user;
-  }
-
   public UsersService service() {
     return session.wsClient().users();
   }
@@ -128,4 +121,14 @@ public class UserTester {
     }
     return Optional.empty();
   }
+
+  public final String generateLogin() {
+    int id = ID_GENERATOR.getAndIncrement();
+    return "login" + id;
+  }
+
+  public final String generateProviderId() {
+    int id = ID_GENERATOR.getAndIncrement();
+    return "providerId" + id;
+  }
 }
index 9e37fd49ed3519689b5ff4d23075181fd8213b55..aff024938eee757e73086d182ebb13e0bb64d10b 100644 (file)
@@ -299,6 +299,10 @@ public class Navigation {
     return new EmailAlreadyExistsPage();
   }
 
+  public UpdateLoginPage asUpdateLoginPage() {
+    return new UpdateLoginPage();
+  }
+
   private static SelenideElement logInLink() {
     return Selenide.$(By.linkText("Log in"));
   }
diff --git a/server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/UpdateLoginPage.java b/server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/UpdateLoginPage.java
new file mode 100644 (file)
index 0000000..2860ad7
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.sonarqube.qa.util.pageobjects;
+
+import static com.codeborne.selenide.Condition.text;
+import static com.codeborne.selenide.Condition.visible;
+import static com.codeborne.selenide.Selenide.$;
+
+public class UpdateLoginPage extends Navigation {
+
+  public UpdateLoginPage shouldHaveProviderName(String providerName) {
+    $(".js-provider-name").shouldHave(text(providerName));
+    return this;
+  }
+
+  public UpdateLoginPage shouldHaveNewAccount(String login) {
+    $(".js-new-account").shouldHave(text(login));
+    return this;
+  }
+
+  public UpdateLoginPage shouldHaveOldLogin(String oldLogin) {
+    $(".js-old-login").shouldHave(text(oldLogin));
+    return this;
+  }
+
+  public UpdateLoginPage shouldHaveOldOrganizationKey(String oldOrganizationKey) {
+    $(".js-old-organization-key").shouldHave(text(oldOrganizationKey));
+    return this;
+  }
+
+  public void clickContinue() {
+    $(".js-continue").click();
+    $(".js-continue").shouldNotBe(visible);
+  }
+
+  public void clickCancel() {
+    $(".js-cancel").click();
+    $(".js-cancel").shouldNotBe(visible);
+  }
+
+}
index 4f256b9b13c0d351f56bbe82deeeda017c79838b..7cc3041bdd34b4afa02fee4324959307e58af62d 100644 (file)
@@ -37,7 +37,7 @@ public class AuthenticationModule extends Module {
       IdentityProviderRepository.class,
       BaseContextFactory.class,
       OAuth2ContextFactory.class,
-      UserIdentityAuthenticator.class,
+      UserIdentityAuthenticatorImpl.class,
       OAuthCsrfVerifier.class,
       UserSessionInitializer.class,
       JwtSerializer.class,
index 27ebcd9ac2adff6e71d555a9acca245d15a22454..1d5cc56802ae5591817c87b5a73dfa767987830a 100644 (file)
@@ -28,13 +28,13 @@ import static java.lang.String.format;
 import static java.net.URLEncoder.encode;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-class AuthenticationRedirection {
+public class AuthenticationRedirection {
 
   private AuthenticationRedirection() {
     // Only static methods
   }
 
-  static String encodeMessage(String message) {
+  public static String encodeMessage(String message) {
     try {
       return encode(message, UTF_8.name());
     } catch (UnsupportedEncodingException e) {
@@ -42,7 +42,7 @@ class AuthenticationRedirection {
     }
   }
 
-  static void redirectTo(HttpServletResponse response, String url) {
+  public static void redirectTo(HttpServletResponse response, String url) {
     try {
       response.sendRedirect(url);
     } catch (IOException e) {
index 8ebb25b8debb2bd18b60637c2fe473781ecdfc54..686dcd83138ab205c47a0c67895c257d0631b206 100644 (file)
@@ -25,12 +25,12 @@ import org.sonar.api.platform.Server;
 import org.sonar.api.server.authentication.BaseIdentityProvider;
 import org.sonar.api.server.authentication.UserIdentity;
 import org.sonar.db.user.UserDto;
+import org.sonar.server.authentication.UserIdentityAuthenticatorParameters.ExistingEmailStrategy;
+import org.sonar.server.authentication.UserIdentityAuthenticatorParameters.UpdateLoginStrategy;
 import org.sonar.server.authentication.event.AuthenticationEvent.Source;
 import org.sonar.server.user.ThreadLocalUserSession;
 import org.sonar.server.user.UserSessionFactory;
 
-import static org.sonar.server.authentication.UserIdentityAuthenticator.ExistingEmailStrategy.FORBID;
-
 public class BaseContextFactory {
 
   private final ThreadLocalUserSession threadLocalUserSession;
@@ -80,7 +80,14 @@ public class BaseContextFactory {
 
     @Override
     public void authenticate(UserIdentity userIdentity) {
-      UserDto userDto = userIdentityAuthenticator.authenticate(userIdentity, identityProvider, Source.external(identityProvider), FORBID);
+      UserDto userDto = userIdentityAuthenticator.authenticate(
+        UserIdentityAuthenticatorParameters.builder()
+          .setUserIdentity(userIdentity)
+          .setProvider(identityProvider)
+          .setSource(Source.external(identityProvider))
+          .setExistingEmailStrategy(ExistingEmailStrategy.FORBID)
+          .setUpdateLoginStrategy(UpdateLoginStrategy.ALLOW)
+          .build());
       jwtHttpHandler.generateToken(userDto, request, response);
       threadLocalUserSession.set(userSessionFactory.create(userDto));
     }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/EmailAlreadyExistsException.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/EmailAlreadyExistsException.java
deleted file mode 100644 (file)
index ec50b9d..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info 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.authentication;
-
-import org.sonar.api.server.authentication.IdentityProvider;
-import org.sonar.api.server.authentication.UserIdentity;
-import org.sonar.db.user.UserDto;
-
-import static java.lang.String.format;
-import static org.sonar.server.authentication.AuthenticationRedirection.encodeMessage;
-
-/**
- * This exception is used to redirect the user to a page explaining him that his email is already used by another account,
- * and where he has the ability to authenticate by "steeling" this email.
- */
-public class EmailAlreadyExistsException extends RuntimeException {
-
-  private static final String PATH = "/sessions/email_already_exists?email=%s&login=%s&provider=%s&existingLogin=%s&existingProvider=%s";
-
-  private final String email;
-  private final UserDto existingUser;
-  private final UserIdentity userIdentity;
-  private final IdentityProvider provider;
-
-  EmailAlreadyExistsException(String email, UserDto existingUser, UserIdentity userIdentity, IdentityProvider provider) {
-    this.email = email;
-    this.existingUser = existingUser;
-    this.userIdentity = userIdentity;
-    this.provider = provider;
-  }
-
-  public String getPath(String contextPath) {
-    return contextPath + format(PATH,
-      encodeMessage(email),
-      encodeMessage(userIdentity.getProviderLogin()),
-      encodeMessage(provider.getKey()),
-      encodeMessage(existingUser.getExternalLogin()),
-      encodeMessage(existingUser.getExternalIdentityProvider()));
-  }
-}
index 40244e387240beb3eb43a4aaf97d3a2b108f3f7f..1f09d4aa38af6d09d7aaf82628aa8a779654e604 100644 (file)
@@ -32,6 +32,7 @@ import org.sonar.api.server.authentication.OAuth2IdentityProvider;
 import org.sonar.api.server.authentication.UnauthorizedException;
 import org.sonar.server.authentication.event.AuthenticationEvent;
 import org.sonar.server.authentication.event.AuthenticationException;
+import org.sonar.server.authentication.exception.RedirectionException;
 
 import static java.lang.String.format;
 import static org.sonar.server.authentication.AuthenticationError.handleAuthenticationError;
@@ -87,7 +88,7 @@ public class InitFilter extends AuthenticationFilter {
       oAuthOAuth2AuthenticationParameters.delete(request, response);
       authenticationEvent.loginFailure(request, e);
       handleAuthenticationError(e, response, getContextPath());
-    } catch (EmailAlreadyExistsException e) {
+    } catch (RedirectionException e) {
       oAuthOAuth2AuthenticationParameters.delete(request, response);
       redirectTo(response, e.getPath(getContextPath()));
     } catch (Exception e) {
index 622b06c6e0fe4b5a69256152961c826c2d46b900..186d8ec53ac6d09101b83b96ee816fa5e14e8ee2 100644 (file)
@@ -39,6 +39,8 @@ public interface OAuth2AuthenticationParameters {
 
   Optional<Boolean> getAllowEmailShift(HttpServletRequest request);
 
+  Optional<Boolean> getAllowUpdateLogin(HttpServletRequest request);
+
   void delete(HttpServletRequest request, HttpServletResponse response);
 
 }
index d7d1eb35bb25bc4ed96d5ff5c6cc2bf0e96c4612..0ef340fb682269a025bce8d82d2a81f43b17107f 100644 (file)
@@ -54,6 +54,11 @@ public class OAuth2AuthenticationParametersImpl implements OAuth2AuthenticationP
    */
   private static final String ALLOW_EMAIL_SHIFT_PARAMETER = "allowEmailShift";
 
+  /**
+   * This parameter is used to allow the update of login
+   */
+  private static final String ALLOW_LOGIN_UPDATE_PARAMETER = "allowUpdateLogin";
+
   private static final Type JSON_MAP_TYPE = new TypeToken<HashMap<String, String>>() {
   }.getType();
 
@@ -61,6 +66,7 @@ public class OAuth2AuthenticationParametersImpl implements OAuth2AuthenticationP
   public void init(HttpServletRequest request, HttpServletResponse response) {
     String returnTo = request.getParameter(RETURN_TO_PARAMETER);
     String allowEmailShift = request.getParameter(ALLOW_EMAIL_SHIFT_PARAMETER);
+    String allowLoginUpdate = request.getParameter(ALLOW_LOGIN_UPDATE_PARAMETER);
     Map<String, String> parameters = new HashMap<>();
     if (isNotBlank(returnTo)) {
       parameters.put(RETURN_TO_PARAMETER, returnTo);
@@ -68,6 +74,9 @@ public class OAuth2AuthenticationParametersImpl implements OAuth2AuthenticationP
     if (isNotBlank(allowEmailShift)) {
       parameters.put(ALLOW_EMAIL_SHIFT_PARAMETER, allowEmailShift);
     }
+    if (isNotBlank(allowLoginUpdate)) {
+      parameters.put(ALLOW_LOGIN_UPDATE_PARAMETER, allowLoginUpdate);
+    }
     if (parameters.isEmpty()) {
       return;
     }
@@ -90,6 +99,12 @@ public class OAuth2AuthenticationParametersImpl implements OAuth2AuthenticationP
     return parameter.map(Boolean::parseBoolean);
   }
 
+  @Override
+  public Optional<Boolean> getAllowUpdateLogin(HttpServletRequest request) {
+    Optional<String> parameter = getParameter(request, ALLOW_LOGIN_UPDATE_PARAMETER);
+    return parameter.map(Boolean::parseBoolean);
+  }
+
   private static Optional<String> getParameter(HttpServletRequest request, String parameterKey) {
     Optional<javax.servlet.http.Cookie> cookie = findCookie(AUTHENTICATION_COOKIE_NAME, request);
     if (!cookie.isPresent()) {
index 114050a7cee75fb122f73380c34de8c408e9f5ce..0712cc0b52d6378b5eeedd8c24422c1ad408350e 100644 (file)
@@ -33,6 +33,7 @@ import org.sonar.api.server.authentication.UnauthorizedException;
 import org.sonar.api.server.authentication.UserIdentity;
 import org.sonar.server.authentication.event.AuthenticationEvent;
 import org.sonar.server.authentication.event.AuthenticationException;
+import org.sonar.server.authentication.exception.RedirectionException;
 
 import static java.lang.String.format;
 import static org.sonar.server.authentication.AuthenticationError.handleAuthenticationError;
@@ -81,7 +82,7 @@ public class OAuth2CallbackFilter extends AuthenticationFilter {
       oauth2Parameters.delete(request, response);
       authenticationEvent.loginFailure(request, e);
       handleAuthenticationError(e, response, getContextPath());
-    } catch (EmailAlreadyExistsException e) {
+    } catch (RedirectionException e) {
       oauth2Parameters.delete(request, response);
       redirectTo(response, e.getPath(getContextPath()));
     } catch (Exception e) {
index 0696ab81f3868ab3e741ed6571562e7e20b207b9..674bc6a562965355c81411dc4f89d017d14d4c4f 100644 (file)
@@ -28,14 +28,14 @@ import org.sonar.api.server.ServerSide;
 import org.sonar.api.server.authentication.OAuth2IdentityProvider;
 import org.sonar.api.server.authentication.UserIdentity;
 import org.sonar.db.user.UserDto;
+import org.sonar.server.authentication.UserIdentityAuthenticatorParameters.ExistingEmailStrategy;
+import org.sonar.server.authentication.UserIdentityAuthenticatorParameters.UpdateLoginStrategy;
 import org.sonar.server.authentication.event.AuthenticationEvent;
 import org.sonar.server.user.ThreadLocalUserSession;
 import org.sonar.server.user.UserSessionFactory;
 
 import static java.lang.String.format;
 import static org.sonar.server.authentication.OAuth2CallbackFilter.CALLBACK_PATH;
-import static org.sonar.server.authentication.UserIdentityAuthenticator.ExistingEmailStrategy.ALLOW;
-import static org.sonar.server.authentication.UserIdentityAuthenticator.ExistingEmailStrategy.WARN;
 
 @ServerSide
 public class OAuth2ContextFactory {
@@ -127,7 +127,15 @@ public class OAuth2ContextFactory {
     @Override
     public void authenticate(UserIdentity userIdentity) {
       Boolean allowEmailShift = oAuthParameters.getAllowEmailShift(request).orElse(false);
-      UserDto userDto = userIdentityAuthenticator.authenticate(userIdentity, identityProvider, AuthenticationEvent.Source.oauth2(identityProvider), allowEmailShift ? ALLOW : WARN);
+      Boolean allowUpdateLogin = oAuthParameters.getAllowUpdateLogin(request).orElse(false);
+      UserDto userDto = userIdentityAuthenticator.authenticate(
+        UserIdentityAuthenticatorParameters.builder()
+          .setUserIdentity(userIdentity)
+          .setProvider(identityProvider)
+          .setSource(AuthenticationEvent.Source.oauth2(identityProvider))
+          .setExistingEmailStrategy(allowEmailShift ? ExistingEmailStrategy.ALLOW : ExistingEmailStrategy.WARN)
+          .setUpdateLoginStrategy(allowUpdateLogin ? UpdateLoginStrategy.ALLOW : UpdateLoginStrategy.WARN)
+          .build());
       jwtHttpHandler.generateToken(userDto, request, response);
       threadLocalUserSession.set(userSessionFactory.create(userDto));
     }
index 8f081b412910535039161157f8891a472191c7f8..fedafa556e16811f9bf1bc4e8f22756d44744453 100644 (file)
@@ -37,6 +37,8 @@ import org.sonar.api.server.authentication.UserIdentity;
 import org.sonar.api.utils.log.Logger;
 import org.sonar.api.utils.log.Loggers;
 import org.sonar.db.user.UserDto;
+import org.sonar.server.authentication.UserIdentityAuthenticatorParameters.ExistingEmailStrategy;
+import org.sonar.server.authentication.UserIdentityAuthenticatorParameters.UpdateLoginStrategy;
 import org.sonar.server.authentication.event.AuthenticationEvent;
 import org.sonar.server.authentication.event.AuthenticationEvent.Source;
 import org.sonar.server.authentication.event.AuthenticationException;
@@ -45,7 +47,6 @@ import org.sonar.server.user.SecurityRealmFactory;
 import static java.util.Objects.requireNonNull;
 import static org.apache.commons.lang.StringUtils.isEmpty;
 import static org.apache.commons.lang.StringUtils.trimToNull;
-import static org.sonar.server.authentication.UserIdentityAuthenticator.ExistingEmailStrategy.FORBID;
 import static org.sonar.server.user.ExternalIdentity.SQ_AUTHORITY;
 
 public class RealmAuthenticator implements Startable {
@@ -139,7 +140,14 @@ public class RealmAuthenticator implements Startable {
       Collection<String> groups = externalGroupsProvider.doGetGroups(context);
       userIdentityBuilder.setGroups(new HashSet<>(groups));
     }
-    return userIdentityAuthenticator.authenticate(userIdentityBuilder.build(), new ExternalIdentityProvider(), realmEventSource(method), FORBID);
+    return userIdentityAuthenticator.authenticate(
+      UserIdentityAuthenticatorParameters.builder()
+        .setUserIdentity(userIdentityBuilder.build())
+        .setProvider(new ExternalIdentityProvider())
+        .setSource(realmEventSource(method))
+        .setExistingEmailStrategy(ExistingEmailStrategy.FORBID)
+        .setUpdateLoginStrategy(UpdateLoginStrategy.ALLOW)
+        .build());
   }
 
   private String getLogin(String userLogin) {
index 691c806b0e0f7594ea634c5011a11646e360318b..8d59e944228445b62892d908fa5febdea7373baa 100644 (file)
@@ -42,6 +42,8 @@ import org.sonar.api.utils.log.Logger;
 import org.sonar.api.utils.log.Loggers;
 import org.sonar.db.user.UserDto;
 import org.sonar.process.ProcessProperties;
+import org.sonar.server.authentication.UserIdentityAuthenticatorParameters.ExistingEmailStrategy;
+import org.sonar.server.authentication.UserIdentityAuthenticatorParameters.UpdateLoginStrategy;
 import org.sonar.server.authentication.event.AuthenticationEvent;
 import org.sonar.server.authentication.event.AuthenticationEvent.Source;
 import org.sonar.server.authentication.event.AuthenticationException;
@@ -54,7 +56,6 @@ import static org.sonar.process.ProcessProperties.Property.SONAR_WEB_SSO_GROUPS_
 import static org.sonar.process.ProcessProperties.Property.SONAR_WEB_SSO_LOGIN_HEADER;
 import static org.sonar.process.ProcessProperties.Property.SONAR_WEB_SSO_NAME_HEADER;
 import static org.sonar.process.ProcessProperties.Property.SONAR_WEB_SSO_REFRESH_INTERVAL_IN_MINUTES;
-import static org.sonar.server.authentication.UserIdentityAuthenticator.ExistingEmailStrategy.FORBID;
 import static org.sonar.server.user.ExternalIdentity.SQ_AUTHORITY;
 
 public class SsoAuthenticator implements Startable {
@@ -161,7 +162,14 @@ public class SsoAuthenticator implements Startable {
       String groupsValue = getHeaderValue(headerValuesByNames, SONAR_WEB_SSO_GROUPS_HEADER.getKey());
       userIdentityBuilder.setGroups(groupsValue == null ? Collections.emptySet() : new HashSet<>(COMA_SPLITTER.splitToList(groupsValue)));
     }
-    return userIdentityAuthenticator.authenticate(userIdentityBuilder.build(), new SsoIdentityProvider(), Source.sso(), FORBID);
+    return userIdentityAuthenticator.authenticate(
+      UserIdentityAuthenticatorParameters.builder()
+        .setUserIdentity(userIdentityBuilder.build())
+        .setProvider(new SsoIdentityProvider())
+        .setSource(Source.sso())
+        .setExistingEmailStrategy(ExistingEmailStrategy.FORBID)
+        .setUpdateLoginStrategy(UpdateLoginStrategy.ALLOW)
+        .build());
   }
 
   @CheckForNull
index b0e18820180ade80e1e5d772b97f891a9cc1a78b..d56ad688867247e1a1d1e6547493f076a1e1afdc 100644 (file)
  * 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.authentication;
 
-import com.google.common.collect.Sets;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import javax.annotation.CheckForNull;
-import javax.annotation.Nullable;
-import org.sonar.api.server.authentication.IdentityProvider;
-import org.sonar.api.server.authentication.UserIdentity;
-import org.sonar.api.utils.log.Logger;
-import org.sonar.api.utils.log.Loggers;
-import org.sonar.db.DbClient;
-import org.sonar.db.DbSession;
-import org.sonar.db.user.GroupDto;
 import org.sonar.db.user.UserDto;
-import org.sonar.db.user.UserGroupDto;
-import org.sonar.server.authentication.event.AuthenticationEvent;
-import org.sonar.server.authentication.event.AuthenticationException;
-import org.sonar.server.organization.DefaultOrganization;
-import org.sonar.server.organization.DefaultOrganizationProvider;
-import org.sonar.server.organization.OrganizationFlags;
-import org.sonar.server.user.ExternalIdentity;
-import org.sonar.server.user.NewUser;
-import org.sonar.server.user.UpdateUser;
-import org.sonar.server.user.UserUpdater;
-import org.sonar.server.usergroups.DefaultGroupFinder;
-
-import static java.lang.String.format;
-import static java.util.Collections.singletonList;
-import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
-
-public class UserIdentityAuthenticator {
-
-  /**
-   * Strategy to be executed when the email of the user is already used by another user
-   */
-  enum ExistingEmailStrategy {
-    /**
-     * Authentication is allowed, the email is moved from other user to current user
-     */
-    ALLOW,
-    /**
-     * Authentication process is stopped, the user is redirected to a page explaining that the email is already used
-     */
-    WARN,
-    /**
-     * Forbid authentication of the user
-     */
-    FORBID
-  }
-
-  private static final Logger LOGGER = Loggers.get(UserIdentityAuthenticator.class);
-
-  private final DbClient dbClient;
-  private final UserUpdater userUpdater;
-  private final DefaultOrganizationProvider defaultOrganizationProvider;
-  private final OrganizationFlags organizationFlags;
-  private final DefaultGroupFinder defaultGroupFinder;
-
-  public UserIdentityAuthenticator(DbClient dbClient, UserUpdater userUpdater, DefaultOrganizationProvider defaultOrganizationProvider, OrganizationFlags organizationFlags,
-    DefaultGroupFinder defaultGroupFinder) {
-    this.dbClient = dbClient;
-    this.userUpdater = userUpdater;
-    this.defaultOrganizationProvider = defaultOrganizationProvider;
-    this.organizationFlags = organizationFlags;
-    this.defaultGroupFinder = defaultGroupFinder;
-  }
-
-  public UserDto authenticate(UserIdentity userIdentity, IdentityProvider provider, AuthenticationEvent.Source source, ExistingEmailStrategy existingEmailStrategy) {
-    try (DbSession dbSession = dbClient.openSession(false)) {
-      UserDto userDto = getUser(dbSession, userIdentity, provider);
-      if (userDto == null) {
-        return registerNewUser(dbSession, null, userIdentity, provider, source, existingEmailStrategy);
-      }
-      if (!userDto.isActive()) {
-        return registerNewUser(dbSession, userDto, userIdentity, provider, source, existingEmailStrategy);
-      }
-      return registerExistingUser(dbSession, userDto, userIdentity, provider, source, existingEmailStrategy);
-    }
-  }
-
-  @CheckForNull
-  private UserDto getUser(DbSession dbSession, UserIdentity userIdentity, IdentityProvider provider) {
-    String externalId = userIdentity.getProviderId();
-    UserDto user = dbClient.userDao().selectByExternalIdAndIdentityProvider(dbSession, externalId == null ? userIdentity.getProviderLogin() : externalId, provider.getKey());
-    // We need to search by login because :
-    // 1. external id may have not been set before,
-    // 2. user may have been provisioned,
-    // 3. user may have been disabled.
-    return user != null ? user : dbClient.userDao().selectByLogin(dbSession, userIdentity.getLogin());
-  }
-
-  private UserDto registerExistingUser(DbSession dbSession, UserDto userDto, UserIdentity identity, IdentityProvider provider, AuthenticationEvent.Source source,
-    ExistingEmailStrategy existingEmailStrategy) {
-    UpdateUser update = new UpdateUser()
-      .setLogin(identity.getLogin())
-      .setEmail(identity.getEmail())
-      .setName(identity.getName())
-      .setExternalIdentity(new ExternalIdentity(provider.getKey(), identity.getProviderLogin(), identity.getProviderId()));
-    Optional<UserDto> otherUserToIndex = validateEmail(dbSession, identity, provider, source, existingEmailStrategy);
-    userUpdater.updateAndCommit(dbSession, userDto, update, u -> syncGroups(dbSession, identity, u), toArray(otherUserToIndex));
-    return userDto;
-  }
-
-  private UserDto registerNewUser(DbSession dbSession, @Nullable UserDto disabledUser, UserIdentity identity, IdentityProvider provider, AuthenticationEvent.Source source,
-    ExistingEmailStrategy existingEmailStrategy) {
-    Optional<UserDto> otherUserToIndex = validateEmail(dbSession, identity, provider, source, existingEmailStrategy);
-    NewUser newUser = createNewUser(identity, provider, source);
-    if (disabledUser == null) {
-      return userUpdater.createAndCommit(dbSession, newUser, u -> syncGroups(dbSession, identity, u), toArray(otherUserToIndex));
-    }
-    return userUpdater.reactivateAndCommit(dbSession, disabledUser, newUser, u -> syncGroups(dbSession, identity, u), toArray(otherUserToIndex));
-  }
-
-  private Optional<UserDto> validateEmail(DbSession dbSession, UserIdentity identity, IdentityProvider provider, AuthenticationEvent.Source source,
-    ExistingEmailStrategy existingEmailStrategy) {
-    String email = identity.getEmail();
-    if (email == null) {
-      return Optional.empty();
-    }
-    UserDto existingUser = dbClient.userDao().selectByEmail(dbSession, email);
-    if (existingUser == null
-      || Objects.equals(existingUser.getLogin(), identity.getLogin())
-      || (Objects.equals(existingUser.getExternalId(), identity.getProviderId()) && Objects.equals(existingUser.getExternalIdentityProvider(), provider.getKey()))) {
-      return Optional.empty();
-    }
-    switch (existingEmailStrategy) {
-      case ALLOW:
-        existingUser.setEmail(null);
-        dbClient.userDao().update(dbSession, existingUser);
-        return Optional.of(existingUser);
-      case WARN:
-        throw new EmailAlreadyExistsException(email, existingUser, identity, provider);
-      case FORBID:
-        throw AuthenticationException.newBuilder()
-          .setSource(source)
-          .setLogin(identity.getLogin())
-          .setMessage(format("Email '%s' is already used", email))
-          .setPublicMessage(format(
-            "You can't sign up because email '%s' is already used by an existing user. This means that you probably already registered with another account.",
-            email))
-          .build();
-      default:
-        throw new IllegalStateException(format("Unknown strategy %s", existingEmailStrategy));
-    }
-  }
-
-  private void syncGroups(DbSession dbSession, UserIdentity userIdentity, UserDto userDto) {
-    if (!userIdentity.shouldSyncGroups()) {
-      return;
-    }
-    String userLogin = userIdentity.getLogin();
-    Set<String> userGroups = new HashSet<>(dbClient.groupMembershipDao().selectGroupsByLogins(dbSession, singletonList(userLogin)).get(userLogin));
-    Set<String> identityGroups = userIdentity.getGroups();
-    LOGGER.debug("List of groups returned by the identity provider '{}'", identityGroups);
-
-    Collection<String> groupsToAdd = Sets.difference(identityGroups, userGroups);
-    Collection<String> groupsToRemove = Sets.difference(userGroups, identityGroups);
-    Collection<String> allGroups = new ArrayList<>(groupsToAdd);
-    allGroups.addAll(groupsToRemove);
-    DefaultOrganization defaultOrganization = defaultOrganizationProvider.get();
-    Map<String, GroupDto> groupsByName = dbClient.groupDao().selectByNames(dbSession, defaultOrganization.getUuid(), allGroups)
-      .stream()
-      .collect(uniqueIndex(GroupDto::getName));
-
-    addGroups(dbSession, userDto, groupsToAdd, groupsByName);
-    removeGroups(dbSession, userDto, groupsToRemove, groupsByName);
-  }
-
-  private void addGroups(DbSession dbSession, UserDto userDto, Collection<String> groupsToAdd, Map<String, GroupDto> groupsByName) {
-    groupsToAdd.stream().map(groupsByName::get).filter(Objects::nonNull).forEach(
-      groupDto -> {
-        LOGGER.debug("Adding group '{}' to user '{}'", groupDto.getName(), userDto.getLogin());
-        dbClient.userGroupDao().insert(dbSession, new UserGroupDto().setGroupId(groupDto.getId()).setUserId(userDto.getId()));
-      });
-  }
-
-  private void removeGroups(DbSession dbSession, UserDto userDto, Collection<String> groupsToRemove, Map<String, GroupDto> groupsByName) {
-    Optional<GroupDto> defaultGroup = getDefaultGroup(dbSession);
-    groupsToRemove.stream().map(groupsByName::get)
-      .filter(Objects::nonNull)
-      // user should be member of default group only when organizations are disabled, as the IdentityProvider API doesn't handle yet
-      // organizations
-      .filter(group -> !defaultGroup.isPresent() || !group.getId().equals(defaultGroup.get().getId()))
-      .forEach(groupDto -> {
-        LOGGER.debug("Removing group '{}' from user '{}'", groupDto.getName(), userDto.getLogin());
-        dbClient.userGroupDao().delete(dbSession, groupDto.getId(), userDto.getId());
-      });
-  }
-
-  private Optional<GroupDto> getDefaultGroup(DbSession dbSession) {
-    return organizationFlags.isEnabled(dbSession) ? Optional.empty() : Optional.of(defaultGroupFinder.findDefaultGroup(dbSession, defaultOrganizationProvider.get().getUuid()));
-  }
 
-  private static NewUser createNewUser(UserIdentity identity, IdentityProvider provider, AuthenticationEvent.Source source) {
-    if (!provider.allowsUsersToSignUp()) {
-      throw AuthenticationException.newBuilder()
-        .setSource(source)
-        .setLogin(identity.getLogin())
-        .setMessage(format("User signup disabled for provider '%s'", provider.getKey()))
-        .setPublicMessage(format("'%s' users are not allowed to sign up", provider.getKey()))
-        .build();
-    }
-    return NewUser.builder()
-      .setLogin(identity.getLogin())
-      .setEmail(identity.getEmail())
-      .setName(identity.getName())
-      .setExternalIdentity(new ExternalIdentity(provider.getKey(), identity.getProviderLogin(), identity.getProviderId()))
-      .build();
-  }
+public interface UserIdentityAuthenticator {
 
-  private static UserDto[] toArray(Optional<UserDto> userDto) {
-    return userDto.map(u -> new UserDto[] {u}).orElse(new UserDto[] {});
-  }
+  UserDto authenticate(UserIdentityAuthenticatorParameters authenticatorParameters);
 
 }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticatorImpl.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticatorImpl.java
new file mode 100644 (file)
index 0000000..966f0d9
--- /dev/null
@@ -0,0 +1,267 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.authentication;
+
+import com.google.common.collect.Sets;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.sonar.api.server.authentication.IdentityProvider;
+import org.sonar.api.server.authentication.UserIdentity;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.organization.OrganizationDto;
+import org.sonar.db.user.GroupDto;
+import org.sonar.db.user.UserDto;
+import org.sonar.db.user.UserGroupDto;
+import org.sonar.server.authentication.UserIdentityAuthenticatorParameters.ExistingEmailStrategy;
+import org.sonar.server.authentication.event.AuthenticationException;
+import org.sonar.server.authentication.exception.EmailAlreadyExistsRedirectionException;
+import org.sonar.server.authentication.exception.UpdateLoginRedirectionException;
+import org.sonar.server.organization.DefaultOrganization;
+import org.sonar.server.organization.DefaultOrganizationProvider;
+import org.sonar.server.organization.OrganizationFlags;
+import org.sonar.server.organization.OrganizationUpdater;
+import org.sonar.server.user.ExternalIdentity;
+import org.sonar.server.user.NewUser;
+import org.sonar.server.user.UpdateUser;
+import org.sonar.server.user.UserUpdater;
+import org.sonar.server.usergroups.DefaultGroupFinder;
+
+import static com.google.common.base.Preconditions.checkState;
+import static java.lang.String.format;
+import static java.util.Collections.singletonList;
+import static java.util.Objects.requireNonNull;
+import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
+import static org.sonar.server.authentication.UserIdentityAuthenticatorParameters.UpdateLoginStrategy;
+
+public class UserIdentityAuthenticatorImpl implements UserIdentityAuthenticator {
+
+  private static final Logger LOGGER = Loggers.get(UserIdentityAuthenticatorImpl.class);
+
+  private final DbClient dbClient;
+  private final UserUpdater userUpdater;
+  private final DefaultOrganizationProvider defaultOrganizationProvider;
+  private final OrganizationFlags organizationFlags;
+  private final OrganizationUpdater organizationUpdater;
+  private final DefaultGroupFinder defaultGroupFinder;
+
+  public UserIdentityAuthenticatorImpl(DbClient dbClient, UserUpdater userUpdater, DefaultOrganizationProvider defaultOrganizationProvider, OrganizationFlags organizationFlags,
+    OrganizationUpdater organizationUpdater, DefaultGroupFinder defaultGroupFinder) {
+    this.dbClient = dbClient;
+    this.userUpdater = userUpdater;
+    this.defaultOrganizationProvider = defaultOrganizationProvider;
+    this.organizationFlags = organizationFlags;
+    this.organizationUpdater = organizationUpdater;
+    this.defaultGroupFinder = defaultGroupFinder;
+  }
+
+  @Override
+  public UserDto authenticate(UserIdentityAuthenticatorParameters authenticatorParameters) {
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      UserDto userDto = getUser(dbSession, authenticatorParameters.getUserIdentity(), authenticatorParameters.getProvider());
+      if (userDto == null) {
+        return registerNewUser(dbSession, null, authenticatorParameters);
+      }
+      if (!userDto.isActive()) {
+        return registerNewUser(dbSession, userDto, authenticatorParameters);
+      }
+      return registerExistingUser(dbSession, userDto, authenticatorParameters);
+    }
+  }
+
+  @CheckForNull
+  private UserDto getUser(DbSession dbSession, UserIdentity userIdentity, IdentityProvider provider) {
+    String externalId = userIdentity.getProviderId();
+    UserDto user = dbClient.userDao().selectByExternalIdAndIdentityProvider(dbSession, externalId == null ? userIdentity.getProviderLogin() : externalId, provider.getKey());
+    // We need to search by login because :
+    // 1. external id may have not been set before,
+    // 2. user may have been provisioned,
+    // 3. user may have been disabled.
+    return user != null ? user : dbClient.userDao().selectByLogin(dbSession, userIdentity.getLogin());
+  }
+
+  private UserDto registerExistingUser(DbSession dbSession, UserDto userDto, UserIdentityAuthenticatorParameters authenticatorParameters) {
+    UpdateUser update = new UpdateUser()
+      .setLogin(authenticatorParameters.getUserIdentity().getLogin())
+      .setEmail(authenticatorParameters.getUserIdentity().getEmail())
+      .setName(authenticatorParameters.getUserIdentity().getName())
+      .setExternalIdentity(new ExternalIdentity(
+        authenticatorParameters.getProvider().getKey(),
+        authenticatorParameters.getUserIdentity().getProviderLogin(),
+        authenticatorParameters.getUserIdentity().getProviderId()));
+    detectLoginUpdate(dbSession, userDto, update, authenticatorParameters);
+    Optional<UserDto> otherUserToIndex = detectEmailUpdate(dbSession, authenticatorParameters);
+    userUpdater.updateAndCommit(dbSession, userDto, update, u -> syncGroups(dbSession, authenticatorParameters.getUserIdentity(), u), toArray(otherUserToIndex));
+    return userDto;
+  }
+
+  private UserDto registerNewUser(DbSession dbSession, @Nullable UserDto disabledUser, UserIdentityAuthenticatorParameters authenticatorParameters) {
+    Optional<UserDto> otherUserToIndex = detectEmailUpdate(dbSession, authenticatorParameters);
+    NewUser newUser = createNewUser(authenticatorParameters);
+    if (disabledUser == null) {
+      return userUpdater.createAndCommit(dbSession, newUser, u -> syncGroups(dbSession, authenticatorParameters.getUserIdentity(), u), toArray(otherUserToIndex));
+    }
+    return userUpdater.reactivateAndCommit(dbSession, disabledUser, newUser, u -> syncGroups(dbSession, authenticatorParameters.getUserIdentity(), u), toArray(otherUserToIndex));
+  }
+
+  private Optional<UserDto> detectEmailUpdate(DbSession dbSession, UserIdentityAuthenticatorParameters authenticatorParameters) {
+    String email = authenticatorParameters.getUserIdentity().getEmail();
+    if (email == null) {
+      return Optional.empty();
+    }
+    UserDto existingUser = dbClient.userDao().selectByEmail(dbSession, email);
+    if (existingUser == null
+      || Objects.equals(existingUser.getLogin(), authenticatorParameters.getUserIdentity().getLogin())
+      || (Objects.equals(existingUser.getExternalId(), authenticatorParameters.getUserIdentity().getProviderId())
+        && Objects.equals(existingUser.getExternalIdentityProvider(), authenticatorParameters.getProvider().getKey()))) {
+      return Optional.empty();
+    }
+    ExistingEmailStrategy existingEmailStrategy = authenticatorParameters.getExistingEmailStrategy();
+    switch (existingEmailStrategy) {
+      case ALLOW:
+        existingUser.setEmail(null);
+        dbClient.userDao().update(dbSession, existingUser);
+        return Optional.of(existingUser);
+      case WARN:
+        throw new EmailAlreadyExistsRedirectionException(email, existingUser, authenticatorParameters.getUserIdentity(), authenticatorParameters.getProvider());
+      case FORBID:
+        throw AuthenticationException.newBuilder()
+          .setSource(authenticatorParameters.getSource())
+          .setLogin(authenticatorParameters.getUserIdentity().getLogin())
+          .setMessage(format("Email '%s' is already used", email))
+          .setPublicMessage(format(
+            "You can't sign up because email '%s' is already used by an existing user. This means that you probably already registered with another account.",
+            email))
+          .build();
+      default:
+        throw new IllegalStateException(format("Unknown strategy %s", existingEmailStrategy));
+    }
+  }
+
+  private void detectLoginUpdate(DbSession dbSession, UserDto user, UpdateUser update, UserIdentityAuthenticatorParameters authenticatorParameters) {
+    String newLogin = update.login();
+    if (!update.isLoginChanged() || user.getLogin().equals(newLogin)) {
+      return;
+    }
+    if (!organizationFlags.isEnabled(dbSession)) {
+      return;
+    }
+    String personalOrganizationUuid = user.getOrganizationUuid();
+    if (personalOrganizationUuid == null) {
+      return;
+    }
+    Optional<OrganizationDto> personalOrganization = dbClient.organizationDao().selectByUuid(dbSession, personalOrganizationUuid);
+    checkState(personalOrganization.isPresent(),
+      "Cannot find personal organization uuid '%s' for user '%s'", personalOrganizationUuid, user.getLogin());
+    UpdateLoginStrategy updateLoginStrategy = authenticatorParameters.getUpdateLoginStrategy();
+    switch (updateLoginStrategy) {
+      case ALLOW:
+        organizationUpdater.updateOrganizationKey(dbSession, personalOrganization.get(), requireNonNull(newLogin, "new login cannot be null"));
+        return;
+      case WARN:
+        throw new UpdateLoginRedirectionException(authenticatorParameters.getUserIdentity(), authenticatorParameters.getProvider(), user, personalOrganization.get());
+      default:
+        throw new IllegalStateException(format("Unknown strategy %s", updateLoginStrategy));
+    }
+  }
+
+  private void syncGroups(DbSession dbSession, UserIdentity userIdentity, UserDto userDto) {
+    if (!userIdentity.shouldSyncGroups()) {
+      return;
+    }
+    String userLogin = userIdentity.getLogin();
+    Set<String> userGroups = new HashSet<>(dbClient.groupMembershipDao().selectGroupsByLogins(dbSession, singletonList(userLogin)).get(userLogin));
+    Set<String> identityGroups = userIdentity.getGroups();
+    LOGGER.debug("List of groups returned by the identity provider '{}'", identityGroups);
+
+    Collection<String> groupsToAdd = Sets.difference(identityGroups, userGroups);
+    Collection<String> groupsToRemove = Sets.difference(userGroups, identityGroups);
+    Collection<String> allGroups = new ArrayList<>(groupsToAdd);
+    allGroups.addAll(groupsToRemove);
+    DefaultOrganization defaultOrganization = defaultOrganizationProvider.get();
+    Map<String, GroupDto> groupsByName = dbClient.groupDao().selectByNames(dbSession, defaultOrganization.getUuid(), allGroups)
+      .stream()
+      .collect(uniqueIndex(GroupDto::getName));
+
+    addGroups(dbSession, userDto, groupsToAdd, groupsByName);
+    removeGroups(dbSession, userDto, groupsToRemove, groupsByName);
+  }
+
+  private void addGroups(DbSession dbSession, UserDto userDto, Collection<String> groupsToAdd, Map<String, GroupDto> groupsByName) {
+    groupsToAdd.stream().map(groupsByName::get).filter(Objects::nonNull).forEach(
+      groupDto -> {
+        LOGGER.debug("Adding group '{}' to user '{}'", groupDto.getName(), userDto.getLogin());
+        dbClient.userGroupDao().insert(dbSession, new UserGroupDto().setGroupId(groupDto.getId()).setUserId(userDto.getId()));
+      });
+  }
+
+  private void removeGroups(DbSession dbSession, UserDto userDto, Collection<String> groupsToRemove, Map<String, GroupDto> groupsByName) {
+    Optional<GroupDto> defaultGroup = getDefaultGroup(dbSession);
+    groupsToRemove.stream().map(groupsByName::get)
+      .filter(Objects::nonNull)
+      // user should be member of default group only when organizations are disabled, as the IdentityProvider API doesn't handle yet
+      // organizations
+      .filter(group -> !defaultGroup.isPresent() || !group.getId().equals(defaultGroup.get().getId()))
+      .forEach(groupDto -> {
+        LOGGER.debug("Removing group '{}' from user '{}'", groupDto.getName(), userDto.getLogin());
+        dbClient.userGroupDao().delete(dbSession, groupDto.getId(), userDto.getId());
+      });
+  }
+
+  private Optional<GroupDto> getDefaultGroup(DbSession dbSession) {
+    return organizationFlags.isEnabled(dbSession) ? Optional.empty() : Optional.of(defaultGroupFinder.findDefaultGroup(dbSession, defaultOrganizationProvider.get().getUuid()));
+  }
+
+  private static NewUser createNewUser(UserIdentityAuthenticatorParameters authenticatorParameters) {
+    String identityProviderKey = authenticatorParameters.getProvider().getKey();
+    if (!authenticatorParameters.getProvider().allowsUsersToSignUp()) {
+      throw AuthenticationException.newBuilder()
+        .setSource(authenticatorParameters.getSource())
+        .setLogin(authenticatorParameters.getUserIdentity().getLogin())
+        .setMessage(format("User signup disabled for provider '%s'", identityProviderKey))
+        .setPublicMessage(format("'%s' users are not allowed to sign up", identityProviderKey))
+        .build();
+    }
+    return NewUser.builder()
+      .setLogin(authenticatorParameters.getUserIdentity().getLogin())
+      .setEmail(authenticatorParameters.getUserIdentity().getEmail())
+      .setName(authenticatorParameters.getUserIdentity().getName())
+      .setExternalIdentity(
+        new ExternalIdentity(
+          identityProviderKey,
+          authenticatorParameters.getUserIdentity().getProviderLogin(),
+          authenticatorParameters.getUserIdentity().getProviderId()))
+      .build();
+  }
+
+  private static UserDto[] toArray(Optional<UserDto> userDto) {
+    return userDto.map(u -> new UserDto[] {u}).orElse(new UserDto[] {});
+  }
+
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticatorParameters.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticatorParameters.java
new file mode 100644 (file)
index 0000000..224946e
--- /dev/null
@@ -0,0 +1,149 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.authentication;
+
+import org.sonar.api.server.authentication.IdentityProvider;
+import org.sonar.api.server.authentication.UserIdentity;
+import org.sonar.server.authentication.event.AuthenticationEvent;
+
+import static java.util.Objects.requireNonNull;
+
+class UserIdentityAuthenticatorParameters {
+
+  /**
+   * Strategy to be executed when the email of the user is already used by another user
+   */
+  enum ExistingEmailStrategy {
+    /**
+     * Authentication is allowed, the email is moved from other user to current user
+     */
+    ALLOW,
+    /**
+     * Authentication process is stopped, the user is redirected to a page explaining that the email is already used
+     */
+    WARN,
+    /**
+     * Forbid authentication of the user
+     */
+    FORBID
+  }
+
+  /**
+   * Strategy to be executed when the login of the user is updated
+   */
+  enum UpdateLoginStrategy {
+    /**
+     * Authentication is allowed, the login of the user updated
+     */
+    ALLOW,
+    /**
+     * Authentication process is stopped, the user is redirected to a page explaining that the login will be updated.
+     * It only happens when personal organizations are activated
+     */
+    WARN
+  }
+
+  private final UserIdentity userIdentity;
+  private final IdentityProvider provider;
+  private final AuthenticationEvent.Source source;
+  private final ExistingEmailStrategy existingEmailStrategy;
+  private final UpdateLoginStrategy updateLoginStrategy;
+
+  UserIdentityAuthenticatorParameters(Builder builder) {
+    this.userIdentity = builder.userIdentity;
+    this.provider = builder.provider;
+    this.source = builder.source;
+    this.existingEmailStrategy = builder.existingEmailStrategy;
+    this.updateLoginStrategy = builder.updateLoginStrategy;
+  }
+
+  public UserIdentity getUserIdentity() {
+    return userIdentity;
+  }
+
+  public IdentityProvider getProvider() {
+    return provider;
+  }
+
+  public AuthenticationEvent.Source getSource() {
+    return source;
+  }
+
+  public ExistingEmailStrategy getExistingEmailStrategy() {
+    return existingEmailStrategy;
+  }
+
+  public UpdateLoginStrategy getUpdateLoginStrategy() {
+    return updateLoginStrategy;
+  }
+
+  static UserIdentityAuthenticatorParameters.Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+    private UserIdentity userIdentity;
+    private IdentityProvider provider;
+    private AuthenticationEvent.Source source;
+    private ExistingEmailStrategy existingEmailStrategy;
+    private UpdateLoginStrategy updateLoginStrategy;
+
+    public Builder setUserIdentity(UserIdentity userIdentity) {
+      this.userIdentity = userIdentity;
+      return this;
+    }
+
+    public Builder setProvider(IdentityProvider provider) {
+      this.provider = provider;
+      return this;
+    }
+
+    public Builder setSource(AuthenticationEvent.Source source) {
+      this.source = source;
+      return this;
+    }
+
+    /**
+     * Strategy to be executed when the email of the user is already used by another user
+     */
+    public Builder setExistingEmailStrategy(ExistingEmailStrategy existingEmailStrategy) {
+      this.existingEmailStrategy = existingEmailStrategy;
+      return this;
+    }
+
+    /**
+     * Strategy to be executed when the login of the user has changed
+     */
+    public Builder setUpdateLoginStrategy(UpdateLoginStrategy updateLoginStrategy) {
+      this.updateLoginStrategy = updateLoginStrategy;
+      return this;
+    }
+
+    public UserIdentityAuthenticatorParameters build() {
+      requireNonNull(userIdentity, "userIdentity must be set");
+      requireNonNull(provider, "identityProvider must be set");
+      requireNonNull(source, "Source must be set");
+      requireNonNull(existingEmailStrategy, "existingEmailStrategy must be set ");
+      requireNonNull(updateLoginStrategy, "updateLoginStrategy must be set");
+      return new UserIdentityAuthenticatorParameters(this);
+    }
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/exception/EmailAlreadyExistsRedirectionException.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/exception/EmailAlreadyExistsRedirectionException.java
new file mode 100644 (file)
index 0000000..da743e0
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.authentication.exception;
+
+import org.sonar.api.server.authentication.IdentityProvider;
+import org.sonar.api.server.authentication.UserIdentity;
+import org.sonar.db.user.UserDto;
+
+import static java.lang.String.format;
+import static org.sonar.server.authentication.AuthenticationRedirection.encodeMessage;
+
+/**
+ * This exception is used to redirect the user to a page explaining him that his email is already used by another account,
+ * and where he has the ability to authenticate by "steeling" this email.
+ */
+public class EmailAlreadyExistsRedirectionException extends RedirectionException {
+
+  private static final String PATH = "/sessions/email_already_exists?email=%s&login=%s&provider=%s&existingLogin=%s&existingProvider=%s";
+
+  private final String email;
+  private final UserDto existingUser;
+  private final UserIdentity userIdentity;
+  private final IdentityProvider provider;
+
+  public EmailAlreadyExistsRedirectionException(String email, UserDto existingUser, UserIdentity userIdentity, IdentityProvider provider) {
+    this.email = email;
+    this.existingUser = existingUser;
+    this.userIdentity = userIdentity;
+    this.provider = provider;
+  }
+
+  @Override
+  public String getPath(String contextPath) {
+    return contextPath + format(PATH,
+      encodeMessage(email),
+      encodeMessage(userIdentity.getProviderLogin()),
+      encodeMessage(provider.getKey()),
+      encodeMessage(existingUser.getExternalLogin()),
+      encodeMessage(existingUser.getExternalIdentityProvider()));
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/exception/RedirectionException.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/exception/RedirectionException.java
new file mode 100644 (file)
index 0000000..7c8687c
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.authentication.exception;
+
+public abstract class RedirectionException extends RuntimeException {
+
+  public abstract String getPath(String contextPath);
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/exception/UpdateLoginRedirectionException.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/exception/UpdateLoginRedirectionException.java
new file mode 100644 (file)
index 0000000..0d08a72
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.authentication.exception;
+
+import org.sonar.api.server.authentication.IdentityProvider;
+import org.sonar.api.server.authentication.UserIdentity;
+import org.sonar.db.organization.OrganizationDto;
+import org.sonar.db.user.UserDto;
+
+import static java.lang.String.format;
+import static org.sonar.server.authentication.AuthenticationRedirection.encodeMessage;
+
+/**
+ * This exception is used to redirect the user to a page explaining him that his login will be updated.
+ */
+public class UpdateLoginRedirectionException extends RedirectionException {
+
+  private static final String PATH = "/sessions/update_login?login=%s&providerKey=%s&providerName=%s&oldLogin=%s&oldOrganizationKey=%s";
+
+  private final UserIdentity userIdentity;
+  private final IdentityProvider provider;
+  private final UserDto user;
+  private final OrganizationDto organization;
+
+  public UpdateLoginRedirectionException(UserIdentity userIdentity, IdentityProvider provider, UserDto user, OrganizationDto organization) {
+    this.userIdentity = userIdentity;
+    this.provider = provider;
+    this.user = user;
+    this.organization = organization;
+  }
+
+  @Override
+  public String getPath(String contextPath) {
+    return contextPath + format(PATH,
+      encodeMessage(userIdentity.getProviderLogin()),
+      encodeMessage(provider.getKey()),
+      encodeMessage(provider.getName()),
+      encodeMessage(user.getLogin()),
+      encodeMessage(organization.getKey()));
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/exception/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/exception/package-info.java
new file mode 100644 (file)
index 0000000..0a87b31
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.authentication.exception;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-server/src/main/java/org/sonar/server/organization/OrganizationCreation.java b/server/sonar-server/src/main/java/org/sonar/server/organization/OrganizationCreation.java
deleted file mode 100644 (file)
index 67b66d3..0000000
+++ /dev/null
@@ -1,215 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info 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.organization;
-
-import java.util.Optional;
-import javax.annotation.CheckForNull;
-import javax.annotation.Nullable;
-import org.sonar.api.web.UserRole;
-import org.sonar.core.permission.GlobalPermissions;
-import org.sonar.db.DbSession;
-import org.sonar.db.organization.OrganizationDto;
-import org.sonar.db.user.UserDto;
-import org.sonar.server.usergroups.DefaultGroupCreatorImpl;
-
-import static java.util.Objects.requireNonNull;
-
-public interface OrganizationCreation {
-  String OWNERS_GROUP_NAME = "Owners";
-  String OWNERS_GROUP_DESCRIPTION_PATTERN = "Owners of organization %s";
-  String PERM_TEMPLATE_NAME = "Default template";
-  String PERM_TEMPLATE_DESCRIPTION_PATTERN = "Default permission template of organization %s";
-  String PERSONAL_ORGANIZATION_DESCRIPTION_PATTERN = "%s's personal organization";
-
-  /**
-   * Create a new non guarded organization with the specified properties and of which the specified user will assign
-   * Administer Organization permission.
-   * <p>
-   * This method does several operations at once:
-   * <ol>
-   *   <li>create an ungarded organization with the specified details</li>
-   *   <li>create a group called {@link #OWNERS_GROUP_NAME Owners} with all organization wide permissions</li>
-   *   <li>create a group called {@link DefaultGroupCreatorImpl#DEFAULT_GROUP_NAME members} with browse permissions</li>
-   *   <li>make the specified user a member of these groups</li>
-   *   <li>create a default template for the organization
-   *       <ul>
-   *         <li>name is {@link #PERM_TEMPLATE_NAME Default template}</li>
-   *         <li>description follows pattern {@link #PERM_TEMPLATE_DESCRIPTION_PATTERN} based on the organization name</li>
-   *       </ul>
-   *   </li>
-   *   <li>this permission template defines the specified permissions (which effectively makes projects public):
-   *     <ul>
-   *       <li>group {@link #OWNERS_GROUP_NAME Owners} : {@link UserRole#ADMIN ADMIN}</li>
-   *       <li>group {@link #OWNERS_GROUP_NAME Owners} : {@link UserRole#ISSUE_ADMIN ISSUE_ADMIN}</li>
-   *       <li>group {@link #OWNERS_GROUP_NAME Owners} : {@link GlobalPermissions#SCAN_EXECUTION SCAN_EXECUTION}</li>
-   *       <li>group {@link DefaultGroupCreatorImpl#DEFAULT_GROUP_NAME members} : {@link UserRole#USER USER}</li>
-   *       <li>group {@link DefaultGroupCreatorImpl#DEFAULT_GROUP_NAME members} : {@link UserRole#CODEVIEWER CODEVIEWER}</li>
-   *     </ul>
-   *   </li>
-   * </ol>
-   * </p>
-   *
-   * @return the created organization
-   *
-   * @throws KeyConflictException if an organization with the specified key already exists
-   * @throws IllegalArgumentException if any field of {@code newOrganization} is invalid according to {@link OrganizationValidation}
-   */
-  OrganizationDto create(DbSession dbSession, UserDto userCreator, NewOrganization newOrganization) throws KeyConflictException;
-
-  /**
-   * Create a new guarded organization which details are based on the login of the specified User.
-   * <p>
-   * This method does several operations at once:
-   * <ol>
-   *   <li>
-   *     create a guarded organization with the details computed from user's details:
-   *     <ul>
-   *       <li>key: generated from the user's login</li>
-   *       <li>name: the user's name if set, otherwise the user's login</li>
-   *       <li>description: {@link #PERSONAL_ORGANIZATION_DESCRIPTION_PATTERN "[name]'s personal organization"} where name
-   *           is user name (when non null and non empty) or login</li>
-   *       <li>url and avatar: null</li>
-   *     </ul>
-   *   </li>
-   *   <li>create a group called {@link DefaultGroupCreatorImpl#DEFAULT_GROUP_NAME members} with browse permissions</li>
-   *   <li>make the specified user a member of this group</li>
-   *   <li>give all organization wide permissions to the user</li>
-   *   <li>create a default template for the organization
-   *       <ul>
-   *         <li>name is {@link #PERM_TEMPLATE_NAME Default template}</li>
-   *         <li>description follows pattern {@link #PERM_TEMPLATE_DESCRIPTION_PATTERN} based on the organization name</li>
-   *       </ul>
-   *   </li>
-   *   <li>this permission template defines the specified permissions (which effectively makes projects public and
-   *       automatically adds new projects to the user's favorites):
-   *     <ul>
-   *       <li>project creator : {@link UserRole#ADMIN ADMIN}</li>
-   *       <li>project creator : {@link UserRole#ISSUE_ADMIN ISSUE_ADMIN}</li>
-   *       <li>project creator : {@link GlobalPermissions#SCAN_EXECUTION SCAN_EXECUTION}</li>
-   *       <li>group {@link DefaultGroupCreatorImpl#DEFAULT_GROUP_NAME members} : {@link UserRole#USER USER}</li>
-   *       <li>group {@link DefaultGroupCreatorImpl#DEFAULT_GROUP_NAME members} : {@link UserRole#CODEVIEWER CODEVIEWER}</li>
-   *     </ul>
-   *   </li>
-   * </ol>
-   * </p>
-   *
-   * @return the created organization or empty if feature is disabled
-   *
-   * @throws IllegalArgumentException if any field of {@code newOrganization} is invalid according to {@link OrganizationValidation}
-   * @throws IllegalStateException if an organization with the key generated from the login already exists
-   */
-  Optional<OrganizationDto> createForUser(DbSession dbSession, UserDto newUser);
-
-  final class KeyConflictException extends Exception {
-    KeyConflictException(String message) {
-      super(message);
-    }
-  }
-
-  final class NewOrganization {
-    private final String key;
-    private final String name;
-    @CheckForNull
-    private final String description;
-    @CheckForNull
-    private final String url;
-    @CheckForNull
-    private final String avatar;
-
-    private NewOrganization(Builder builder) {
-      this.key = builder.key;
-      this.name = builder.name;
-      this.description = builder.description;
-      this.url = builder.url;
-      this.avatar = builder.avatarUrl;
-    }
-
-    public String getKey() {
-      return key;
-    }
-
-    public String getName() {
-      return name;
-    }
-
-    @CheckForNull
-    public String getDescription() {
-      return description;
-    }
-
-    @CheckForNull
-    public String getUrl() {
-      return url;
-    }
-
-    @CheckForNull
-    public String getAvatar() {
-      return avatar;
-    }
-
-    public static NewOrganization.Builder newOrganizationBuilder() {
-      return new Builder();
-    }
-
-    public static final class Builder {
-      private String key;
-      private String name;
-      private String description;
-      private String url;
-      private String avatarUrl;
-
-      private Builder() {
-        // use factory method
-      }
-
-      public Builder setKey(String key) {
-        this.key = requireNonNull(key, "key can't be null");
-        return this;
-      }
-
-      public Builder setName(String name) {
-        this.name = requireNonNull(name, "name can't be null");
-        return this;
-      }
-
-      public Builder setDescription(@Nullable String description) {
-        this.description = description;
-        return this;
-      }
-
-      public Builder setUrl(@Nullable String url) {
-        this.url = url;
-        return this;
-      }
-
-      public Builder setAvatarUrl(@Nullable String avatarUrl) {
-        this.avatarUrl = avatarUrl;
-        return this;
-      }
-
-      public NewOrganization build() {
-        requireNonNull(key, "key can't be null");
-        requireNonNull(name, "name can't be null");
-        return new NewOrganization(this);
-      }
-    }
-  }
-
-}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/organization/OrganizationCreationImpl.java b/server/sonar-server/src/main/java/org/sonar/server/organization/OrganizationCreationImpl.java
deleted file mode 100644 (file)
index 78c0f8d..0000000
+++ /dev/null
@@ -1,335 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info 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.organization;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.function.Consumer;
-import javax.annotation.Nullable;
-import org.sonar.api.config.Configuration;
-import org.sonar.api.utils.System2;
-import org.sonar.core.config.CorePropertyDefinitions;
-import org.sonar.core.util.UuidFactory;
-import org.sonar.db.DbClient;
-import org.sonar.db.DbSession;
-import org.sonar.db.organization.DefaultTemplates;
-import org.sonar.db.organization.OrganizationDto;
-import org.sonar.db.organization.OrganizationMemberDto;
-import org.sonar.db.permission.GroupPermissionDto;
-import org.sonar.db.permission.OrganizationPermission;
-import org.sonar.db.permission.UserPermissionDto;
-import org.sonar.db.permission.template.PermissionTemplateCharacteristicDto;
-import org.sonar.db.permission.template.PermissionTemplateDto;
-import org.sonar.db.qualitygate.QualityGateDto;
-import org.sonar.db.qualityprofile.DefaultQProfileDto;
-import org.sonar.db.qualityprofile.OrgQProfileDto;
-import org.sonar.db.user.GroupDto;
-import org.sonar.db.user.UserDto;
-import org.sonar.db.user.UserGroupDto;
-import org.sonar.server.qualityprofile.BuiltInQProfile;
-import org.sonar.server.qualityprofile.BuiltInQProfileRepository;
-import org.sonar.server.qualityprofile.QProfileName;
-import org.sonar.server.user.index.UserIndexer;
-import org.sonar.server.usergroups.DefaultGroupCreator;
-
-import static com.google.common.base.Preconditions.checkState;
-import static java.lang.String.format;
-import static java.util.Objects.requireNonNull;
-import static org.sonar.api.web.UserRole.ADMIN;
-import static org.sonar.api.web.UserRole.CODEVIEWER;
-import static org.sonar.api.web.UserRole.ISSUE_ADMIN;
-import static org.sonar.api.web.UserRole.USER;
-import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
-import static org.sonar.db.permission.OrganizationPermission.SCAN;
-import static org.sonar.server.organization.OrganizationCreation.NewOrganization.newOrganizationBuilder;
-
-public class OrganizationCreationImpl implements OrganizationCreation {
-
-  private final DbClient dbClient;
-  private final System2 system2;
-  private final UuidFactory uuidFactory;
-  private final OrganizationValidation organizationValidation;
-  private final Configuration config;
-  private final BuiltInQProfileRepository builtInQProfileRepository;
-  private final DefaultGroupCreator defaultGroupCreator;
-  private final UserIndexer userIndexer;
-
-  public OrganizationCreationImpl(DbClient dbClient, System2 system2, UuidFactory uuidFactory,
-    OrganizationValidation organizationValidation, Configuration config, UserIndexer userIndexer,
-    BuiltInQProfileRepository builtInQProfileRepository, DefaultGroupCreator defaultGroupCreator) {
-    this.dbClient = dbClient;
-    this.system2 = system2;
-    this.uuidFactory = uuidFactory;
-    this.organizationValidation = organizationValidation;
-    this.config = config;
-    this.userIndexer = userIndexer;
-    this.builtInQProfileRepository = builtInQProfileRepository;
-    this.defaultGroupCreator = defaultGroupCreator;
-  }
-
-  @Override
-  public OrganizationDto create(DbSession dbSession, UserDto userCreator, NewOrganization newOrganization) throws KeyConflictException {
-    validate(newOrganization);
-    String key = newOrganization.getKey();
-    if (organizationKeyIsUsed(dbSession, key)) {
-      throw new KeyConflictException(format("Organization key '%s' is already used", key));
-    }
-
-    QualityGateDto builtInQualityGate = dbClient.qualityGateDao().selectBuiltIn(dbSession);
-    OrganizationDto organization = insertOrganization(dbSession, newOrganization, builtInQualityGate);
-    insertOrganizationMember(dbSession, organization, userCreator.getId());
-    dbClient.qualityGateDao().associate(dbSession, uuidFactory.create(), organization, builtInQualityGate);
-    GroupDto ownerGroup = insertOwnersGroup(dbSession, organization);
-    GroupDto defaultGroup = defaultGroupCreator.create(dbSession, organization.getUuid());
-    insertDefaultTemplateOnGroups(dbSession, organization, ownerGroup, defaultGroup);
-    try (DbSession batchDbSession = dbClient.openSession(true)) {
-      insertQualityProfiles(dbSession, batchDbSession, organization);
-      addCurrentUserToGroup(dbSession, ownerGroup, userCreator.getId());
-      addCurrentUserToGroup(dbSession, defaultGroup, userCreator.getId());
-
-      batchDbSession.commit();
-
-      // Elasticsearch is updated when DB session is committed
-      userIndexer.commitAndIndex(dbSession, userCreator);
-
-      return organization;
-    }
-  }
-
-  @Override
-  public Optional<OrganizationDto> createForUser(DbSession dbSession, UserDto newUser) {
-    if (!isCreatePersonalOrgEnabled()) {
-      return Optional.empty();
-    }
-
-    String nameOrLogin = nameOrLogin(newUser);
-    NewOrganization newOrganization = newOrganizationBuilder()
-      .setKey(organizationValidation.generateKeyFrom(newUser.getLogin()))
-      .setName(toName(nameOrLogin))
-      .setDescription(format(PERSONAL_ORGANIZATION_DESCRIPTION_PATTERN, nameOrLogin))
-      .build();
-    checkState(!organizationKeyIsUsed(dbSession, newOrganization.getKey()),
-      "Can't create organization with key '%s' for new user '%s' because an organization with this key already exists",
-      newOrganization.getKey(),
-      newUser.getLogin());
-
-    QualityGateDto builtInQualityGate = dbClient.qualityGateDao().selectBuiltIn(dbSession);
-    OrganizationDto organization = insertOrganization(dbSession, newOrganization, builtInQualityGate,
-      dto -> dto.setGuarded(true).setUserId(newUser.getId()));
-    insertOrganizationMember(dbSession, organization, newUser.getId());
-    GroupDto defaultGroup = defaultGroupCreator.create(dbSession, organization.getUuid());
-    dbClient.qualityGateDao().associate(dbSession, uuidFactory.create(), organization, builtInQualityGate);
-    OrganizationPermission.all()
-      .forEach(p -> insertUserPermissions(dbSession, newUser, organization, p));
-    insertPersonalOrgDefaultTemplate(dbSession, organization, defaultGroup);
-    try (DbSession batchDbSession = dbClient.openSession(true)) {
-      insertQualityProfiles(dbSession, batchDbSession, organization);
-      addCurrentUserToGroup(dbSession, defaultGroup, newUser.getId());
-
-      batchDbSession.commit();
-
-      // Elasticsearch is updated when DB session is committed
-      userIndexer.commitAndIndex(dbSession, newUser);
-
-      return Optional.of(organization);
-    }
-  }
-
-  private static String nameOrLogin(UserDto newUser) {
-    String name = newUser.getName();
-    if (name == null || name.isEmpty()) {
-      return newUser.getLogin();
-    }
-    return name;
-  }
-
-  private String toName(String login) {
-    String name = login.substring(0, Math.min(login.length(), OrganizationValidation.NAME_MAX_LENGTH));
-    // should not happen has login can't be less than 2 chars, but we call it for safety
-    organizationValidation.checkName(name);
-    return name;
-  }
-
-  private boolean isCreatePersonalOrgEnabled() {
-    return config.getBoolean(CorePropertyDefinitions.ORGANIZATIONS_CREATE_PERSONAL_ORG).orElse(false);
-  }
-
-  private void validate(NewOrganization newOrganization) {
-    requireNonNull(newOrganization, "newOrganization can't be null");
-    organizationValidation.checkName(newOrganization.getName());
-    organizationValidation.checkKey(newOrganization.getKey());
-    organizationValidation.checkDescription(newOrganization.getDescription());
-    organizationValidation.checkUrl(newOrganization.getUrl());
-    organizationValidation.checkAvatar(newOrganization.getAvatar());
-  }
-
-  private OrganizationDto insertOrganization(DbSession dbSession, NewOrganization newOrganization, QualityGateDto builtInQualityGate, Consumer<OrganizationDto>... extendCreation) {
-    OrganizationDto res = new OrganizationDto()
-      .setUuid(uuidFactory.create())
-      .setName(newOrganization.getName())
-      .setKey(newOrganization.getKey())
-      .setDescription(newOrganization.getDescription())
-      .setUrl(newOrganization.getUrl())
-      .setDefaultQualityGateUuid(builtInQualityGate.getUuid())
-      .setAvatarUrl(newOrganization.getAvatar());
-    Arrays.stream(extendCreation).forEach(c -> c.accept(res));
-    dbClient.organizationDao().insert(dbSession, res, false);
-    return res;
-  }
-
-  private boolean organizationKeyIsUsed(DbSession dbSession, String key) {
-    return dbClient.organizationDao().selectByKey(dbSession, key).isPresent();
-  }
-
-  private void insertDefaultTemplateOnGroups(DbSession dbSession, OrganizationDto organizationDto, GroupDto ownerGroup, GroupDto defaultGroup) {
-    Date now = new Date(system2.now());
-    PermissionTemplateDto permissionTemplateDto = dbClient.permissionTemplateDao().insert(
-      dbSession,
-      new PermissionTemplateDto()
-        .setOrganizationUuid(organizationDto.getUuid())
-        .setUuid(uuidFactory.create())
-        .setName(PERM_TEMPLATE_NAME)
-        .setDescription(format(PERM_TEMPLATE_DESCRIPTION_PATTERN, organizationDto.getName()))
-        .setCreatedAt(now)
-        .setUpdatedAt(now));
-
-    insertGroupPermission(dbSession, permissionTemplateDto, ADMIN, ownerGroup);
-    insertGroupPermission(dbSession, permissionTemplateDto, ISSUE_ADMIN, ownerGroup);
-    insertGroupPermission(dbSession, permissionTemplateDto, SCAN.getKey(), ownerGroup);
-    insertGroupPermission(dbSession, permissionTemplateDto, USER, defaultGroup);
-    insertGroupPermission(dbSession, permissionTemplateDto, CODEVIEWER, defaultGroup);
-
-    dbClient.organizationDao().setDefaultTemplates(
-      dbSession,
-      organizationDto.getUuid(),
-      new DefaultTemplates().setProjectUuid(permissionTemplateDto.getUuid()));
-  }
-
-  private void insertPersonalOrgDefaultTemplate(DbSession dbSession, OrganizationDto organizationDto, GroupDto defaultGroup) {
-    long now = system2.now();
-    Date dateNow = new Date(now);
-    PermissionTemplateDto permissionTemplateDto = dbClient.permissionTemplateDao().insert(
-      dbSession,
-      new PermissionTemplateDto()
-        .setOrganizationUuid(organizationDto.getUuid())
-        .setUuid(uuidFactory.create())
-        .setName("Default template")
-        .setDescription(format(PERM_TEMPLATE_DESCRIPTION_PATTERN, organizationDto.getName()))
-        .setCreatedAt(dateNow)
-        .setUpdatedAt(dateNow));
-
-    insertProjectCreatorPermission(dbSession, permissionTemplateDto, ADMIN, now);
-    insertProjectCreatorPermission(dbSession, permissionTemplateDto, ISSUE_ADMIN, now);
-    insertProjectCreatorPermission(dbSession, permissionTemplateDto, SCAN.getKey(), now);
-    insertGroupPermission(dbSession, permissionTemplateDto, USER, defaultGroup);
-    insertGroupPermission(dbSession, permissionTemplateDto, CODEVIEWER, defaultGroup);
-
-    dbClient.organizationDao().setDefaultTemplates(
-      dbSession,
-      organizationDto.getUuid(),
-      new DefaultTemplates().setProjectUuid(permissionTemplateDto.getUuid()));
-  }
-
-  private void insertProjectCreatorPermission(DbSession dbSession, PermissionTemplateDto permissionTemplateDto, String permission, long now) {
-    dbClient.permissionTemplateCharacteristicDao().insert(
-      dbSession,
-      new PermissionTemplateCharacteristicDto()
-        .setTemplateId(permissionTemplateDto.getId())
-        .setWithProjectCreator(true)
-        .setPermission(permission)
-        .setCreatedAt(now)
-        .setUpdatedAt(now));
-  }
-
-  private void insertGroupPermission(DbSession dbSession, PermissionTemplateDto template, String permission, @Nullable GroupDto group) {
-    dbClient.permissionTemplateDao().insertGroupPermission(dbSession, template.getId(), group == null ? null : group.getId(), permission);
-  }
-
-  private void insertQualityProfiles(DbSession dbSession, DbSession batchDbSession, OrganizationDto organization) {
-    Map<QProfileName, BuiltInQProfile> builtInsPerName = builtInQProfileRepository.get().stream()
-      .collect(uniqueIndex(BuiltInQProfile::getQProfileName));
-
-    List<DefaultQProfileDto> defaults = new ArrayList<>();
-    dbClient.qualityProfileDao().selectBuiltInRulesProfiles(dbSession).forEach(rulesProfile -> {
-      OrgQProfileDto dto = new OrgQProfileDto()
-        .setOrganizationUuid(organization.getUuid())
-        .setRulesProfileUuid(rulesProfile.getKee())
-        .setUuid(uuidFactory.create());
-
-      QProfileName name = new QProfileName(rulesProfile.getLanguage(), rulesProfile.getName());
-      BuiltInQProfile builtIn = builtInsPerName.get(name);
-      if (builtIn != null && builtIn.isDefault()) {
-        // rows of table default_qprofiles must be inserted after org_qprofiles
-        // in order to benefit from batch SQL inserts
-        defaults.add(new DefaultQProfileDto()
-          .setQProfileUuid(dto.getUuid())
-          .setOrganizationUuid(organization.getUuid())
-          .setLanguage(builtIn.getLanguage()));
-      }
-
-      dbClient.qualityProfileDao().insert(batchDbSession, dto);
-    });
-
-    defaults.forEach(defaultQProfileDto -> dbClient.defaultQProfileDao().insertOrUpdate(dbSession, defaultQProfileDto));
-  }
-
-  /**
-   * Owners group has an hard coded name, a description based on the organization's name and has all global permissions.
-   */
-  private GroupDto insertOwnersGroup(DbSession dbSession, OrganizationDto organization) {
-    GroupDto group = dbClient.groupDao().insert(dbSession, new GroupDto()
-      .setOrganizationUuid(organization.getUuid())
-      .setName(OWNERS_GROUP_NAME)
-      .setDescription(format(OWNERS_GROUP_DESCRIPTION_PATTERN, organization.getName())));
-    OrganizationPermission.all().forEach(p -> addPermissionToGroup(dbSession, group, p));
-    return group;
-  }
-
-  private void addPermissionToGroup(DbSession dbSession, GroupDto group, OrganizationPermission permission) {
-    dbClient.groupPermissionDao().insert(
-      dbSession,
-      new GroupPermissionDto()
-        .setOrganizationUuid(group.getOrganizationUuid())
-        .setGroupId(group.getId())
-        .setRole(permission.getKey()));
-  }
-
-  private void insertUserPermissions(DbSession dbSession, UserDto userDto, OrganizationDto organization, OrganizationPermission permission) {
-    dbClient.userPermissionDao().insert(
-      dbSession,
-      new UserPermissionDto(organization.getUuid(), permission.getKey(), userDto.getId(), null));
-  }
-
-  private void addCurrentUserToGroup(DbSession dbSession, GroupDto group, int createUserId) {
-    dbClient.userGroupDao().insert(
-      dbSession,
-      new UserGroupDto().setGroupId(group.getId()).setUserId(createUserId));
-  }
-
-  private void insertOrganizationMember(DbSession dbSession, OrganizationDto organizationDto, int userId) {
-    dbClient.organizationMemberDao().insert(dbSession, new OrganizationMemberDto()
-      .setOrganizationUuid(organizationDto.getUuid())
-      .setUserId(userId));
-  }
-}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/organization/OrganizationUpdater.java b/server/sonar-server/src/main/java/org/sonar/server/organization/OrganizationUpdater.java
new file mode 100644 (file)
index 0000000..5748e20
--- /dev/null
@@ -0,0 +1,225 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.organization;
+
+import java.util.Optional;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.sonar.api.web.UserRole;
+import org.sonar.core.permission.GlobalPermissions;
+import org.sonar.db.DbSession;
+import org.sonar.db.organization.OrganizationDto;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.usergroups.DefaultGroupCreatorImpl;
+
+import static java.util.Objects.requireNonNull;
+
+public interface OrganizationUpdater {
+  String OWNERS_GROUP_NAME = "Owners";
+  String OWNERS_GROUP_DESCRIPTION_PATTERN = "Owners of organization %s";
+  String PERM_TEMPLATE_NAME = "Default template";
+  String PERM_TEMPLATE_DESCRIPTION_PATTERN = "Default permission template of organization %s";
+  String PERSONAL_ORGANIZATION_DESCRIPTION_PATTERN = "%s's personal organization";
+
+  /**
+   * Create a new non guarded organization with the specified properties and of which the specified user will assign
+   * Administer Organization permission.
+   * <p>
+   * This method does several operations at once:
+   * <ol>
+   *   <li>create an ungarded organization with the specified details</li>
+   *   <li>create a group called {@link #OWNERS_GROUP_NAME Owners} with all organization wide permissions</li>
+   *   <li>create a group called {@link DefaultGroupCreatorImpl#DEFAULT_GROUP_NAME members} with browse permissions</li>
+   *   <li>make the specified user a member of these groups</li>
+   *   <li>create a default template for the organization
+   *       <ul>
+   *         <li>name is {@link #PERM_TEMPLATE_NAME Default template}</li>
+   *         <li>description follows pattern {@link #PERM_TEMPLATE_DESCRIPTION_PATTERN} based on the organization name</li>
+   *       </ul>
+   *   </li>
+   *   <li>this permission template defines the specified permissions (which effectively makes projects public):
+   *     <ul>
+   *       <li>group {@link #OWNERS_GROUP_NAME Owners} : {@link UserRole#ADMIN ADMIN}</li>
+   *       <li>group {@link #OWNERS_GROUP_NAME Owners} : {@link UserRole#ISSUE_ADMIN ISSUE_ADMIN}</li>
+   *       <li>group {@link #OWNERS_GROUP_NAME Owners} : {@link GlobalPermissions#SCAN_EXECUTION SCAN_EXECUTION}</li>
+   *       <li>group {@link DefaultGroupCreatorImpl#DEFAULT_GROUP_NAME members} : {@link UserRole#USER USER}</li>
+   *       <li>group {@link DefaultGroupCreatorImpl#DEFAULT_GROUP_NAME members} : {@link UserRole#CODEVIEWER CODEVIEWER}</li>
+   *     </ul>
+   *   </li>
+   * </ol>
+   * </p>
+   *
+   * @return the created organization
+   *
+   * @throws KeyConflictException if an organization with the specified key already exists
+   * @throws IllegalArgumentException if any field of {@code newOrganization} is invalid according to {@link OrganizationValidation}
+   */
+  OrganizationDto create(DbSession dbSession, UserDto userCreator, NewOrganization newOrganization) throws KeyConflictException;
+
+  /**
+   * Create a new guarded organization which details are based on the login of the specified User.
+   * <p>
+   * This method does several operations at once:
+   * <ol>
+   *   <li>
+   *     create a guarded organization with the details computed from user's details:
+   *     <ul>
+   *       <li>key: generated from the user's login</li>
+   *       <li>name: the user's name if set, otherwise the user's login</li>
+   *       <li>description: {@link #PERSONAL_ORGANIZATION_DESCRIPTION_PATTERN "[name]'s personal organization"} where name
+   *           is user name (when non null and non empty) or login</li>
+   *       <li>url and avatar: null</li>
+   *     </ul>
+   *   </li>
+   *   <li>create a group called {@link DefaultGroupCreatorImpl#DEFAULT_GROUP_NAME members} with browse permissions</li>
+   *   <li>make the specified user a member of this group</li>
+   *   <li>give all organization wide permissions to the user</li>
+   *   <li>create a default template for the organization
+   *       <ul>
+   *         <li>name is {@link #PERM_TEMPLATE_NAME Default template}</li>
+   *         <li>description follows pattern {@link #PERM_TEMPLATE_DESCRIPTION_PATTERN} based on the organization name</li>
+   *       </ul>
+   *   </li>
+   *   <li>this permission template defines the specified permissions (which effectively makes projects public and
+   *       automatically adds new projects to the user's favorites):
+   *     <ul>
+   *       <li>project creator : {@link UserRole#ADMIN ADMIN}</li>
+   *       <li>project creator : {@link UserRole#ISSUE_ADMIN ISSUE_ADMIN}</li>
+   *       <li>project creator : {@link GlobalPermissions#SCAN_EXECUTION SCAN_EXECUTION}</li>
+   *       <li>group {@link DefaultGroupCreatorImpl#DEFAULT_GROUP_NAME members} : {@link UserRole#USER USER}</li>
+   *       <li>group {@link DefaultGroupCreatorImpl#DEFAULT_GROUP_NAME members} : {@link UserRole#CODEVIEWER CODEVIEWER}</li>
+   *     </ul>
+   *   </li>
+   * </ol>
+   * </p>
+   *
+   * @return the created organization or empty if feature is disabled
+   *
+   * @throws IllegalArgumentException if any field of {@code newOrganization} is invalid according to {@link OrganizationValidation}
+   * @throws IllegalStateException if an organization with the key generated from the login already exists
+   */
+  Optional<OrganizationDto> createForUser(DbSession dbSession, UserDto newUser);
+
+  /**
+   * Update the personal organization key of a user.
+   * No update will be performed if generated key match the same key as existing one.
+   *
+   * @throws IllegalStateException if user has no no personal organization
+   * @throws IllegalStateException if personal organization uuid does not exist
+   * @throws IllegalStateException if an organization with the key generated from the login already exists
+   */
+  void updateOrganizationKey(DbSession dbSession, OrganizationDto organization, String newKey);
+
+  final class KeyConflictException extends Exception {
+    KeyConflictException(String message) {
+      super(message);
+    }
+  }
+
+  final class NewOrganization {
+    private final String key;
+    private final String name;
+    @CheckForNull
+    private final String description;
+    @CheckForNull
+    private final String url;
+    @CheckForNull
+    private final String avatar;
+
+    private NewOrganization(Builder builder) {
+      this.key = builder.key;
+      this.name = builder.name;
+      this.description = builder.description;
+      this.url = builder.url;
+      this.avatar = builder.avatarUrl;
+    }
+
+    public String getKey() {
+      return key;
+    }
+
+    public String getName() {
+      return name;
+    }
+
+    @CheckForNull
+    public String getDescription() {
+      return description;
+    }
+
+    @CheckForNull
+    public String getUrl() {
+      return url;
+    }
+
+    @CheckForNull
+    public String getAvatar() {
+      return avatar;
+    }
+
+    public static NewOrganization.Builder newOrganizationBuilder() {
+      return new Builder();
+    }
+
+    public static final class Builder {
+      private String key;
+      private String name;
+      private String description;
+      private String url;
+      private String avatarUrl;
+
+      private Builder() {
+        // use factory method
+      }
+
+      public Builder setKey(String key) {
+        this.key = requireNonNull(key, "key can't be null");
+        return this;
+      }
+
+      public Builder setName(String name) {
+        this.name = requireNonNull(name, "name can't be null");
+        return this;
+      }
+
+      public Builder setDescription(@Nullable String description) {
+        this.description = description;
+        return this;
+      }
+
+      public Builder setUrl(@Nullable String url) {
+        this.url = url;
+        return this;
+      }
+
+      public Builder setAvatarUrl(@Nullable String avatarUrl) {
+        this.avatarUrl = avatarUrl;
+        return this;
+      }
+
+      public NewOrganization build() {
+        requireNonNull(key, "key can't be null");
+        requireNonNull(name, "name can't be null");
+        return new NewOrganization(this);
+      }
+    }
+  }
+
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/organization/OrganizationUpdaterImpl.java b/server/sonar-server/src/main/java/org/sonar/server/organization/OrganizationUpdaterImpl.java
new file mode 100644 (file)
index 0000000..e53971d
--- /dev/null
@@ -0,0 +1,348 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.organization;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Consumer;
+import javax.annotation.Nullable;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.utils.System2;
+import org.sonar.core.config.CorePropertyDefinitions;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.organization.DefaultTemplates;
+import org.sonar.db.organization.OrganizationDto;
+import org.sonar.db.organization.OrganizationMemberDto;
+import org.sonar.db.permission.GroupPermissionDto;
+import org.sonar.db.permission.OrganizationPermission;
+import org.sonar.db.permission.UserPermissionDto;
+import org.sonar.db.permission.template.PermissionTemplateCharacteristicDto;
+import org.sonar.db.permission.template.PermissionTemplateDto;
+import org.sonar.db.qualitygate.QualityGateDto;
+import org.sonar.db.qualityprofile.DefaultQProfileDto;
+import org.sonar.db.qualityprofile.OrgQProfileDto;
+import org.sonar.db.user.GroupDto;
+import org.sonar.db.user.UserDto;
+import org.sonar.db.user.UserGroupDto;
+import org.sonar.server.qualityprofile.BuiltInQProfile;
+import org.sonar.server.qualityprofile.BuiltInQProfileRepository;
+import org.sonar.server.qualityprofile.QProfileName;
+import org.sonar.server.user.index.UserIndexer;
+import org.sonar.server.usergroups.DefaultGroupCreator;
+
+import static com.google.common.base.Preconditions.checkState;
+import static java.lang.String.format;
+import static java.util.Objects.requireNonNull;
+import static org.sonar.api.web.UserRole.ADMIN;
+import static org.sonar.api.web.UserRole.CODEVIEWER;
+import static org.sonar.api.web.UserRole.ISSUE_ADMIN;
+import static org.sonar.api.web.UserRole.USER;
+import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
+import static org.sonar.db.permission.OrganizationPermission.SCAN;
+import static org.sonar.server.organization.OrganizationUpdater.NewOrganization.newOrganizationBuilder;
+
+public class OrganizationUpdaterImpl implements OrganizationUpdater {
+
+  private final DbClient dbClient;
+  private final System2 system2;
+  private final UuidFactory uuidFactory;
+  private final OrganizationValidation organizationValidation;
+  private final Configuration config;
+  private final BuiltInQProfileRepository builtInQProfileRepository;
+  private final DefaultGroupCreator defaultGroupCreator;
+  private final UserIndexer userIndexer;
+
+  public OrganizationUpdaterImpl(DbClient dbClient, System2 system2, UuidFactory uuidFactory,
+    OrganizationValidation organizationValidation, Configuration config, UserIndexer userIndexer,
+    BuiltInQProfileRepository builtInQProfileRepository, DefaultGroupCreator defaultGroupCreator) {
+    this.dbClient = dbClient;
+    this.system2 = system2;
+    this.uuidFactory = uuidFactory;
+    this.organizationValidation = organizationValidation;
+    this.config = config;
+    this.userIndexer = userIndexer;
+    this.builtInQProfileRepository = builtInQProfileRepository;
+    this.defaultGroupCreator = defaultGroupCreator;
+  }
+
+  @Override
+  public OrganizationDto create(DbSession dbSession, UserDto userCreator, NewOrganization newOrganization) throws KeyConflictException {
+    validate(newOrganization);
+    String key = newOrganization.getKey();
+    if (organizationKeyIsUsed(dbSession, key)) {
+      throw new KeyConflictException(format("Organization key '%s' is already used", key));
+    }
+
+    QualityGateDto builtInQualityGate = dbClient.qualityGateDao().selectBuiltIn(dbSession);
+    OrganizationDto organization = insertOrganization(dbSession, newOrganization, builtInQualityGate);
+    insertOrganizationMember(dbSession, organization, userCreator.getId());
+    dbClient.qualityGateDao().associate(dbSession, uuidFactory.create(), organization, builtInQualityGate);
+    GroupDto ownerGroup = insertOwnersGroup(dbSession, organization);
+    GroupDto defaultGroup = defaultGroupCreator.create(dbSession, organization.getUuid());
+    insertDefaultTemplateOnGroups(dbSession, organization, ownerGroup, defaultGroup);
+    try (DbSession batchDbSession = dbClient.openSession(true)) {
+      insertQualityProfiles(dbSession, batchDbSession, organization);
+      addCurrentUserToGroup(dbSession, ownerGroup, userCreator.getId());
+      addCurrentUserToGroup(dbSession, defaultGroup, userCreator.getId());
+
+      batchDbSession.commit();
+
+      // Elasticsearch is updated when DB session is committed
+      userIndexer.commitAndIndex(dbSession, userCreator);
+
+      return organization;
+    }
+  }
+
+  @Override
+  public Optional<OrganizationDto> createForUser(DbSession dbSession, UserDto newUser) {
+    if (!isCreatePersonalOrgEnabled()) {
+      return Optional.empty();
+    }
+
+    String nameOrLogin = nameOrLogin(newUser);
+    NewOrganization newOrganization = newOrganizationBuilder()
+      .setKey(organizationValidation.generateKeyFrom(newUser.getLogin()))
+      .setName(toName(nameOrLogin))
+      .setDescription(format(PERSONAL_ORGANIZATION_DESCRIPTION_PATTERN, nameOrLogin))
+      .build();
+    checkKey(dbSession, newOrganization.getKey());
+
+    QualityGateDto builtInQualityGate = dbClient.qualityGateDao().selectBuiltIn(dbSession);
+    OrganizationDto organization = insertOrganization(dbSession, newOrganization, builtInQualityGate,
+      dto -> dto.setGuarded(true));
+    dbClient.userDao().update(dbSession, newUser.setOrganizationUuid(organization.getUuid()));
+    insertOrganizationMember(dbSession, organization, newUser.getId());
+    GroupDto defaultGroup = defaultGroupCreator.create(dbSession, organization.getUuid());
+    dbClient.qualityGateDao().associate(dbSession, uuidFactory.create(), organization, builtInQualityGate);
+    OrganizationPermission.all()
+      .forEach(p -> insertUserPermissions(dbSession, newUser, organization, p));
+    insertPersonalOrgDefaultTemplate(dbSession, organization, defaultGroup);
+    try (DbSession batchDbSession = dbClient.openSession(true)) {
+      insertQualityProfiles(dbSession, batchDbSession, organization);
+      addCurrentUserToGroup(dbSession, defaultGroup, newUser.getId());
+
+      batchDbSession.commit();
+
+      // Elasticsearch is updated when DB session is committed
+      userIndexer.commitAndIndex(dbSession, newUser);
+
+      return Optional.of(organization);
+    }
+  }
+
+  @Override
+  public void updateOrganizationKey(DbSession dbSession, OrganizationDto organization, String newKey) {
+    String sanitizedKey = organizationValidation.generateKeyFrom(newKey);
+    if (organization.getKey().equals(sanitizedKey)) {
+      return;
+    }
+    checkKey(dbSession, sanitizedKey);
+    dbClient.organizationDao().update(dbSession, organization.setKey(sanitizedKey));
+  }
+
+  private void checkKey(DbSession dbSession, String key) {
+    checkState(!organizationKeyIsUsed(dbSession, key),
+      "Can't create organization with key '%s' because an organization with this key already exists", key);
+  }
+
+  private static String nameOrLogin(UserDto newUser) {
+    String name = newUser.getName();
+    if (name == null || name.isEmpty()) {
+      return newUser.getLogin();
+    }
+    return name;
+  }
+
+  private String toName(String login) {
+    String name = login.substring(0, Math.min(login.length(), OrganizationValidation.NAME_MAX_LENGTH));
+    // should not happen has login can't be less than 2 chars, but we call it for safety
+    organizationValidation.checkName(name);
+    return name;
+  }
+
+  private boolean isCreatePersonalOrgEnabled() {
+    return config.getBoolean(CorePropertyDefinitions.ORGANIZATIONS_CREATE_PERSONAL_ORG).orElse(false);
+  }
+
+  private void validate(NewOrganization newOrganization) {
+    requireNonNull(newOrganization, "newOrganization can't be null");
+    organizationValidation.checkName(newOrganization.getName());
+    organizationValidation.checkKey(newOrganization.getKey());
+    organizationValidation.checkDescription(newOrganization.getDescription());
+    organizationValidation.checkUrl(newOrganization.getUrl());
+    organizationValidation.checkAvatar(newOrganization.getAvatar());
+  }
+
+  private OrganizationDto insertOrganization(DbSession dbSession, NewOrganization newOrganization, QualityGateDto builtInQualityGate, Consumer<OrganizationDto>... extendCreation) {
+    OrganizationDto res = new OrganizationDto()
+      .setUuid(uuidFactory.create())
+      .setName(newOrganization.getName())
+      .setKey(newOrganization.getKey())
+      .setDescription(newOrganization.getDescription())
+      .setUrl(newOrganization.getUrl())
+      .setDefaultQualityGateUuid(builtInQualityGate.getUuid())
+      .setAvatarUrl(newOrganization.getAvatar());
+    Arrays.stream(extendCreation).forEach(c -> c.accept(res));
+    dbClient.organizationDao().insert(dbSession, res, false);
+    return res;
+  }
+
+  private boolean organizationKeyIsUsed(DbSession dbSession, String key) {
+    return dbClient.organizationDao().selectByKey(dbSession, key).isPresent();
+  }
+
+  private void insertDefaultTemplateOnGroups(DbSession dbSession, OrganizationDto organizationDto, GroupDto ownerGroup, GroupDto defaultGroup) {
+    Date now = new Date(system2.now());
+    PermissionTemplateDto permissionTemplateDto = dbClient.permissionTemplateDao().insert(
+      dbSession,
+      new PermissionTemplateDto()
+        .setOrganizationUuid(organizationDto.getUuid())
+        .setUuid(uuidFactory.create())
+        .setName(PERM_TEMPLATE_NAME)
+        .setDescription(format(PERM_TEMPLATE_DESCRIPTION_PATTERN, organizationDto.getName()))
+        .setCreatedAt(now)
+        .setUpdatedAt(now));
+
+    insertGroupPermission(dbSession, permissionTemplateDto, ADMIN, ownerGroup);
+    insertGroupPermission(dbSession, permissionTemplateDto, ISSUE_ADMIN, ownerGroup);
+    insertGroupPermission(dbSession, permissionTemplateDto, SCAN.getKey(), ownerGroup);
+    insertGroupPermission(dbSession, permissionTemplateDto, USER, defaultGroup);
+    insertGroupPermission(dbSession, permissionTemplateDto, CODEVIEWER, defaultGroup);
+
+    dbClient.organizationDao().setDefaultTemplates(
+      dbSession,
+      organizationDto.getUuid(),
+      new DefaultTemplates().setProjectUuid(permissionTemplateDto.getUuid()));
+  }
+
+  private void insertPersonalOrgDefaultTemplate(DbSession dbSession, OrganizationDto organizationDto, GroupDto defaultGroup) {
+    long now = system2.now();
+    Date dateNow = new Date(now);
+    PermissionTemplateDto permissionTemplateDto = dbClient.permissionTemplateDao().insert(
+      dbSession,
+      new PermissionTemplateDto()
+        .setOrganizationUuid(organizationDto.getUuid())
+        .setUuid(uuidFactory.create())
+        .setName("Default template")
+        .setDescription(format(PERM_TEMPLATE_DESCRIPTION_PATTERN, organizationDto.getName()))
+        .setCreatedAt(dateNow)
+        .setUpdatedAt(dateNow));
+
+    insertProjectCreatorPermission(dbSession, permissionTemplateDto, ADMIN, now);
+    insertProjectCreatorPermission(dbSession, permissionTemplateDto, ISSUE_ADMIN, now);
+    insertProjectCreatorPermission(dbSession, permissionTemplateDto, SCAN.getKey(), now);
+    insertGroupPermission(dbSession, permissionTemplateDto, USER, defaultGroup);
+    insertGroupPermission(dbSession, permissionTemplateDto, CODEVIEWER, defaultGroup);
+
+    dbClient.organizationDao().setDefaultTemplates(
+      dbSession,
+      organizationDto.getUuid(),
+      new DefaultTemplates().setProjectUuid(permissionTemplateDto.getUuid()));
+  }
+
+  private void insertProjectCreatorPermission(DbSession dbSession, PermissionTemplateDto permissionTemplateDto, String permission, long now) {
+    dbClient.permissionTemplateCharacteristicDao().insert(
+      dbSession,
+      new PermissionTemplateCharacteristicDto()
+        .setTemplateId(permissionTemplateDto.getId())
+        .setWithProjectCreator(true)
+        .setPermission(permission)
+        .setCreatedAt(now)
+        .setUpdatedAt(now));
+  }
+
+  private void insertGroupPermission(DbSession dbSession, PermissionTemplateDto template, String permission, @Nullable GroupDto group) {
+    dbClient.permissionTemplateDao().insertGroupPermission(dbSession, template.getId(), group == null ? null : group.getId(), permission);
+  }
+
+  private void insertQualityProfiles(DbSession dbSession, DbSession batchDbSession, OrganizationDto organization) {
+    Map<QProfileName, BuiltInQProfile> builtInsPerName = builtInQProfileRepository.get().stream()
+      .collect(uniqueIndex(BuiltInQProfile::getQProfileName));
+
+    List<DefaultQProfileDto> defaults = new ArrayList<>();
+    dbClient.qualityProfileDao().selectBuiltInRulesProfiles(dbSession).forEach(rulesProfile -> {
+      OrgQProfileDto dto = new OrgQProfileDto()
+        .setOrganizationUuid(organization.getUuid())
+        .setRulesProfileUuid(rulesProfile.getKee())
+        .setUuid(uuidFactory.create());
+
+      QProfileName name = new QProfileName(rulesProfile.getLanguage(), rulesProfile.getName());
+      BuiltInQProfile builtIn = builtInsPerName.get(name);
+      if (builtIn != null && builtIn.isDefault()) {
+        // rows of table default_qprofiles must be inserted after org_qprofiles
+        // in order to benefit from batch SQL inserts
+        defaults.add(new DefaultQProfileDto()
+          .setQProfileUuid(dto.getUuid())
+          .setOrganizationUuid(organization.getUuid())
+          .setLanguage(builtIn.getLanguage()));
+      }
+
+      dbClient.qualityProfileDao().insert(batchDbSession, dto);
+    });
+
+    defaults.forEach(defaultQProfileDto -> dbClient.defaultQProfileDao().insertOrUpdate(dbSession, defaultQProfileDto));
+  }
+
+  /**
+   * Owners group has an hard coded name, a description based on the organization's name and has all global permissions.
+   */
+  private GroupDto insertOwnersGroup(DbSession dbSession, OrganizationDto organization) {
+    GroupDto group = dbClient.groupDao().insert(dbSession, new GroupDto()
+      .setOrganizationUuid(organization.getUuid())
+      .setName(OWNERS_GROUP_NAME)
+      .setDescription(format(OWNERS_GROUP_DESCRIPTION_PATTERN, organization.getName())));
+    OrganizationPermission.all().forEach(p -> addPermissionToGroup(dbSession, group, p));
+    return group;
+  }
+
+  private void addPermissionToGroup(DbSession dbSession, GroupDto group, OrganizationPermission permission) {
+    dbClient.groupPermissionDao().insert(
+      dbSession,
+      new GroupPermissionDto()
+        .setOrganizationUuid(group.getOrganizationUuid())
+        .setGroupId(group.getId())
+        .setRole(permission.getKey()));
+  }
+
+  private void insertUserPermissions(DbSession dbSession, UserDto userDto, OrganizationDto organization, OrganizationPermission permission) {
+    dbClient.userPermissionDao().insert(
+      dbSession,
+      new UserPermissionDto(organization.getUuid(), permission.getKey(), userDto.getId(), null));
+  }
+
+  private void addCurrentUserToGroup(DbSession dbSession, GroupDto group, int createUserId) {
+    dbClient.userGroupDao().insert(
+      dbSession,
+      new UserGroupDto().setGroupId(group.getId()).setUserId(createUserId));
+  }
+
+  private void insertOrganizationMember(DbSession dbSession, OrganizationDto organizationDto, int userId) {
+    dbClient.organizationMemberDao().insert(dbSession, new OrganizationMemberDto()
+      .setOrganizationUuid(organizationDto.getUuid())
+      .setUserId(userId));
+  }
+}
index 6781548044439f355a9f34b0330e29fba2e492fd..894bb7e10eba6c561f548e4cebc3c4b047474438 100644 (file)
@@ -31,14 +31,14 @@ import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.organization.OrganizationDto;
 import org.sonar.db.user.UserDto;
-import org.sonar.server.organization.OrganizationCreation;
 import org.sonar.server.organization.OrganizationFlags;
+import org.sonar.server.organization.OrganizationUpdater;
 import org.sonar.server.organization.OrganizationValidation;
 import org.sonar.server.user.UserSession;
 import org.sonarqube.ws.Organizations.CreateWsResponse;
 
 import static com.google.common.base.Preconditions.checkArgument;
-import static org.sonar.server.organization.OrganizationCreation.NewOrganization.newOrganizationBuilder;
+import static org.sonar.server.organization.OrganizationUpdater.NewOrganization.newOrganizationBuilder;
 import static org.sonar.server.organization.OrganizationValidation.KEY_MAX_LENGTH;
 import static org.sonar.server.organization.ws.OrganizationsWsSupport.PARAM_KEY;
 import static org.sonar.server.ws.WsUtils.writeProtobuf;
@@ -51,17 +51,17 @@ public class CreateAction implements OrganizationsWsAction {
   private final DbClient dbClient;
   private final OrganizationsWsSupport wsSupport;
   private final OrganizationValidation organizationValidation;
-  private final OrganizationCreation organizationCreation;
+  private final OrganizationUpdater organizationUpdater;
   private final OrganizationFlags organizationFlags;
 
   public CreateAction(Configuration config, UserSession userSession, DbClient dbClient, OrganizationsWsSupport wsSupport,
-    OrganizationValidation organizationValidation, OrganizationCreation organizationCreation, OrganizationFlags organizationFlags) {
+                      OrganizationValidation organizationValidation, OrganizationUpdater organizationUpdater, OrganizationFlags organizationFlags) {
     this.config = config;
     this.userSession = userSession;
     this.dbClient = dbClient;
     this.wsSupport = wsSupport;
     this.organizationValidation = organizationValidation;
-    this.organizationCreation = organizationCreation;
+    this.organizationUpdater = organizationUpdater;
     this.organizationFlags = organizationFlags;
   }
 
@@ -107,7 +107,7 @@ public class CreateAction implements OrganizationsWsAction {
     try (DbSession dbSession = dbClient.openSession(false)) {
       organizationFlags.checkEnabled(dbSession);
       UserDto currentUser = dbClient.userDao().selectActiveUserByLogin(dbSession, userSession.getLogin());
-      OrganizationDto organization = organizationCreation.create(
+      OrganizationDto organization = organizationUpdater.create(
         dbSession,
         currentUser,
         newOrganizationBuilder()
@@ -119,7 +119,7 @@ public class CreateAction implements OrganizationsWsAction {
           .build());
 
       writeResponse(request, response, organization);
-    } catch (OrganizationCreation.KeyConflictException e) {
+    } catch (OrganizationUpdater.KeyConflictException e) {
       checkArgument(requestKey == null, "Key '%s' is already used. Specify another one.", key);
       checkArgument(requestKey != null, "Key '%s' generated from name '%s' is already used. Specify one.", key, name);
     }
index 5e090f7be8511998eebdd900dae26ff9a31ed029..e9988fbc7c9084df10f2614a29dfbdcfc0f39577 100644 (file)
@@ -100,7 +100,7 @@ import org.sonar.server.metric.ws.MetricsWsModule;
 import org.sonar.server.notification.NotificationModule;
 import org.sonar.server.notification.ws.NotificationWsModule;
 import org.sonar.server.organization.BillingValidationsProxyImpl;
-import org.sonar.server.organization.OrganizationCreationImpl;
+import org.sonar.server.organization.OrganizationUpdaterImpl;
 import org.sonar.server.organization.OrganizationValidationImpl;
 import org.sonar.server.organization.ws.OrganizationsWsModule;
 import org.sonar.server.permission.GroupPermissionChanger;
@@ -295,7 +295,7 @@ public class PlatformLevel4 extends PlatformLevel {
 
       // organizations
       OrganizationValidationImpl.class,
-      OrganizationCreationImpl.class,
+      OrganizationUpdaterImpl.class,
       OrganizationsWsModule.class,
       BillingValidationsProxyImpl.class,
 
index 7483cc7912eaffa01399b91772be6d5ccac50e25..d968fdf9da5af4c7451572c77525e015de9fa260 100644 (file)
@@ -40,8 +40,8 @@ import org.sonar.db.user.UserDto;
 import org.sonar.db.user.UserGroupDto;
 import org.sonar.server.authentication.LocalAuthentication;
 import org.sonar.server.organization.DefaultOrganizationProvider;
-import org.sonar.server.organization.OrganizationCreation;
 import org.sonar.server.organization.OrganizationFlags;
+import org.sonar.server.organization.OrganizationUpdater;
 import org.sonar.server.user.index.UserIndexer;
 import org.sonar.server.usergroups.DefaultGroupFinder;
 import org.sonar.server.util.Validation;
@@ -77,20 +77,20 @@ public class UserUpdater {
   private final UserIndexer userIndexer;
   private final OrganizationFlags organizationFlags;
   private final DefaultOrganizationProvider defaultOrganizationProvider;
-  private final OrganizationCreation organizationCreation;
+  private final OrganizationUpdater organizationUpdater;
   private final DefaultGroupFinder defaultGroupFinder;
   private final Configuration config;
   private final LocalAuthentication localAuthentication;
 
   public UserUpdater(NewUserNotifier newUserNotifier, DbClient dbClient, UserIndexer userIndexer, OrganizationFlags organizationFlags,
-    DefaultOrganizationProvider defaultOrganizationProvider, OrganizationCreation organizationCreation, DefaultGroupFinder defaultGroupFinder, Configuration config,
-    LocalAuthentication localAuthentication) {
+                     DefaultOrganizationProvider defaultOrganizationProvider, OrganizationUpdater organizationUpdater, DefaultGroupFinder defaultGroupFinder, Configuration config,
+                     LocalAuthentication localAuthentication) {
     this.newUserNotifier = newUserNotifier;
     this.dbClient = dbClient;
     this.userIndexer = userIndexer;
     this.organizationFlags = organizationFlags;
     this.defaultOrganizationProvider = defaultOrganizationProvider;
-    this.organizationCreation = organizationCreation;
+    this.organizationUpdater = organizationUpdater;
     this.defaultGroupFinder = defaultGroupFinder;
     this.config = config;
     this.localAuthentication = localAuthentication;
@@ -393,7 +393,7 @@ public class UserUpdater {
     userDto.setActive(true);
     UserDto res = dbClient.userDao().insert(dbSession, userDto);
     addUserToDefaultOrganizationAndDefaultGroup(dbSession, userDto);
-    organizationCreation.createForUser(dbSession, userDto);
+    organizationUpdater.createForUser(dbSession, userDto);
     return res;
   }
 
index 684cb7974ba961abe13316238baf2f365e28a1af..70cd7748e26917b9eed0bc7e45f128d3ee949de1 100644 (file)
@@ -23,15 +23,12 @@ import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpSession;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
 import org.sonar.api.platform.Server;
 import org.sonar.api.server.authentication.BaseIdentityProvider;
 import org.sonar.api.server.authentication.UserIdentity;
-import org.sonar.api.utils.System2;
-import org.sonar.db.DbTester;
 import org.sonar.db.user.UserDto;
-import org.sonar.server.authentication.event.AuthenticationEvent.Source;
 import org.sonar.server.user.TestUserSessionFactory;
 import org.sonar.server.user.ThreadLocalUserSession;
 import org.sonar.server.user.UserSession;
@@ -42,25 +39,22 @@ import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
-import static org.sonar.server.authentication.UserIdentityAuthenticator.ExistingEmailStrategy.FORBID;
 
 public class BaseContextFactoryTest {
 
   private static final String PUBLIC_ROOT_URL = "https://mydomain.com";
 
   private static final UserIdentity USER_IDENTITY = UserIdentity.builder()
+    .setProviderId("ABCD")
     .setProviderLogin("johndoo")
     .setLogin("id:johndoo")
     .setName("John")
     .setEmail("john@email.com")
     .build();
 
-  @Rule
-  public DbTester dbTester = DbTester.create(System2.INSTANCE);
-
   private ThreadLocalUserSession threadLocalUserSession = mock(ThreadLocalUserSession.class);
 
-  private UserIdentityAuthenticator userIdentityAuthenticator = mock(UserIdentityAuthenticator.class);
+  private TestUserIdentityAuthenticator userIdentityAuthenticator = new TestUserIdentityAuthenticator();
   private Server server = mock(Server.class);
 
   private HttpServletRequest request = mock(HttpServletRequest.class);
@@ -74,7 +68,8 @@ public class BaseContextFactoryTest {
   @Before
   public void setUp() throws Exception {
     when(server.getPublicRootUrl()).thenReturn(PUBLIC_ROOT_URL);
-    when(identityProvider.getName()).thenReturn("provIdeur Nameuh");
+    when(identityProvider.getName()).thenReturn("GitHub");
+    when(identityProvider.getKey()).thenReturn("github");
     when(request.getSession()).thenReturn(mock(HttpSession.class));
   }
 
@@ -89,15 +84,18 @@ public class BaseContextFactoryTest {
 
   @Test
   public void authenticate() {
-    UserDto userDto = dbTester.users().insertUser();
-    when(userIdentityAuthenticator.authenticate(USER_IDENTITY, identityProvider, Source.external(identityProvider), FORBID)).thenReturn(userDto);
     BaseIdentityProvider.Context context = underTest.newContext(request, response, identityProvider);
+    ArgumentCaptor<UserDto> userArgumentCaptor = ArgumentCaptor.forClass(UserDto.class);
 
     context.authenticate(USER_IDENTITY);
 
-    verify(userIdentityAuthenticator).authenticate(USER_IDENTITY, identityProvider, Source.external(identityProvider), FORBID);
-    verify(jwtHttpHandler).generateToken(any(UserDto.class), eq(request), eq(response));
+    assertThat(userIdentityAuthenticator.isAuthenticated()).isTrue();
     verify(threadLocalUserSession).set(any(UserSession.class));
+    verify(jwtHttpHandler).generateToken(userArgumentCaptor.capture(), eq(request), eq(response));
+    assertThat(userArgumentCaptor.getValue().getLogin()).isEqualTo(USER_IDENTITY.getLogin());
+    assertThat(userArgumentCaptor.getValue().getExternalId()).isEqualTo(USER_IDENTITY.getProviderId());
+    assertThat(userArgumentCaptor.getValue().getExternalLogin()).isEqualTo(USER_IDENTITY.getProviderLogin());
+    assertThat(userArgumentCaptor.getValue().getExternalIdentityProvider()).isEqualTo("github");
   }
 
 }
index cc4ff5cd62bb56c1d7105252d748837e24c86de8..c4b4dd7d0b79db901e61714db3e6f8379a625e6e 100644 (file)
@@ -39,6 +39,7 @@ import org.sonar.api.utils.log.LoggerLevel;
 import org.sonar.db.user.UserDto;
 import org.sonar.server.authentication.event.AuthenticationEvent;
 import org.sonar.server.authentication.event.AuthenticationException;
+import org.sonar.server.authentication.exception.EmailAlreadyExistsRedirectionException;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.ArgumentMatchers.eq;
@@ -297,7 +298,7 @@ public class InitFilterTest {
 
     @Override
     public void init(Context context) {
-      throw new EmailAlreadyExistsException(existingUser.getEmail(), existingUser, UserIdentity.builder()
+      throw new EmailAlreadyExistsRedirectionException(existingUser.getEmail(), existingUser, UserIdentity.builder()
         .setProviderLogin("john.github")
         .setLogin("john.github")
         .setName(existingUser.getName())
index 49adf58319a7fc72735ba9cff22370cf6866cca2..56bc3caf086fa6b6d811b108ba888c30ad0c4948 100644 (file)
@@ -44,7 +44,7 @@ public class OAuth2AuthenticationParametersImplTest {
   private HttpServletResponse response = mock(HttpServletResponse.class);
   private HttpServletRequest request = mock(HttpServletRequest.class);
 
-  private OAuth2AuthenticationParametersImpl underTest = new OAuth2AuthenticationParametersImpl();
+  private OAuth2AuthenticationParameters underTest = new OAuth2AuthenticationParametersImpl();
 
   @Before
   public void setUp() throws Exception {
@@ -52,9 +52,8 @@ public class OAuth2AuthenticationParametersImplTest {
   }
 
   @Test
-  public void init_create_cookie_containing_parameters_from_request() {
+  public void init_create_cookie() {
     when(request.getParameter("return_to")).thenReturn("/settings");
-    when(request.getParameter("allowEmailShift")).thenReturn("true");
 
     underTest.init(request, response);
 
@@ -79,6 +78,7 @@ public class OAuth2AuthenticationParametersImplTest {
   public void init_does_not_create_cookie_when_parameters_are_empty() {
     when(request.getParameter("return_to")).thenReturn("");
     when(request.getParameter("allowEmailShift")).thenReturn("");
+    when(request.getParameter("allowUpdateLogin")).thenReturn("");
 
     underTest.init(request, response);
 
@@ -89,6 +89,7 @@ public class OAuth2AuthenticationParametersImplTest {
   public void init_does_not_create_cookie_when_parameters_are_null() {
     when(request.getParameter("return_to")).thenReturn(null);
     when(request.getParameter("allowEmailShift")).thenReturn(null);
+    when(request.getParameter("allowUpdateLogin")).thenReturn(null);
 
     underTest.init(request, response);
 
@@ -151,6 +152,34 @@ public class OAuth2AuthenticationParametersImplTest {
     assertThat(allowEmailShift).isEmpty();
   }
 
+  @Test
+  public void getAllowUpdateLogin() {
+    when(request.getCookies()).thenReturn(new Cookie[] {new Cookie(AUTHENTICATION_COOKIE_NAME, "{\"allowUpdateLogin\":\"true\"}")});
+
+    Optional<Boolean> allowLoginUpdate = underTest.getAllowUpdateLogin(request);
+
+    assertThat(allowLoginUpdate).isNotEmpty();
+    assertThat(allowLoginUpdate.get()).isTrue();
+  }
+
+  @Test
+  public void getAllowUpdateLogin_is_empty_when_no_cookie() {
+    when(request.getCookies()).thenReturn(new Cookie[] {});
+
+    Optional<Boolean> allowLoginUpdate = underTest.getAllowUpdateLogin(request);
+
+    assertThat(allowLoginUpdate).isEmpty();
+  }
+
+  @Test
+  public void getAllowUpdateLogin_is_empty_when_no_value() {
+    when(request.getCookies()).thenReturn(new Cookie[] {new Cookie(AUTHENTICATION_COOKIE_NAME, "{}")});
+
+    Optional<Boolean> allowLoginUpdate = underTest.getAllowUpdateLogin(request);
+
+    assertThat(allowLoginUpdate).isEmpty();
+  }
+
   @Test
   public void delete() {
     when(request.getCookies()).thenReturn(new Cookie[] {new Cookie(AUTHENTICATION_COOKIE_NAME, "{\"return_to\":\"/settings\"}")});
index dcdde1b1610f3179c05bb48710d828af2fbc03cd..b394b99790124732da38139484c2c3a840d38414 100644 (file)
@@ -36,6 +36,7 @@ import org.sonar.api.utils.log.LoggerLevel;
 import org.sonar.db.user.UserDto;
 import org.sonar.server.authentication.event.AuthenticationEvent;
 import org.sonar.server.authentication.event.AuthenticationException;
+import org.sonar.server.authentication.exception.EmailAlreadyExistsRedirectionException;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.ArgumentMatchers.eq;
@@ -250,7 +251,7 @@ public class OAuth2CallbackFilterTest {
 
     @Override
     public void callback(CallbackContext context) {
-      throw new EmailAlreadyExistsException(existingUser.getEmail(), existingUser, UserIdentity.builder()
+      throw new EmailAlreadyExistsRedirectionException(existingUser.getEmail(), existingUser, UserIdentity.builder()
         .setProviderLogin("john.github")
         .setLogin("john.github")
         .setName(existingUser.getName())
index 48fe5e65c015fe4d110aecc2f15acc9bf9712b3a..77dc77f459e313af9c9447220fdc59c91bd8a478 100644 (file)
@@ -27,12 +27,13 @@ import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
+import org.mockito.ArgumentCaptor;
 import org.sonar.api.platform.Server;
 import org.sonar.api.server.authentication.OAuth2IdentityProvider;
 import org.sonar.api.server.authentication.UserIdentity;
-import org.sonar.api.utils.System2;
-import org.sonar.db.DbTester;
 import org.sonar.db.user.UserDto;
+import org.sonar.server.authentication.UserIdentityAuthenticatorParameters.ExistingEmailStrategy;
+import org.sonar.server.authentication.UserIdentityAuthenticatorParameters.UpdateLoginStrategy;
 import org.sonar.server.user.TestUserSessionFactory;
 import org.sonar.server.user.ThreadLocalUserSession;
 import org.sonar.server.user.UserSession;
@@ -43,9 +44,6 @@ import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
-import static org.sonar.server.authentication.UserIdentityAuthenticator.ExistingEmailStrategy.ALLOW;
-import static org.sonar.server.authentication.UserIdentityAuthenticator.ExistingEmailStrategy.WARN;
-import static org.sonar.server.authentication.event.AuthenticationEvent.Source;
 
 public class OAuth2ContextFactoryTest {
 
@@ -53,6 +51,7 @@ public class OAuth2ContextFactoryTest {
   private static final String SECURED_PUBLIC_ROOT_URL = "https://mydomain.com";
   private static final String PROVIDER_NAME = "provider name";
   private static final UserIdentity USER_IDENTITY = UserIdentity.builder()
+    .setProviderId("ABCD")
     .setProviderLogin("johndoo")
     .setLogin("id:johndoo")
     .setName("John")
@@ -62,11 +61,8 @@ public class OAuth2ContextFactoryTest {
   @Rule
   public ExpectedException thrown = ExpectedException.none();
 
-  @Rule
-  public DbTester dbTester = DbTester.create(System2.INSTANCE);
-
   private ThreadLocalUserSession threadLocalUserSession = mock(ThreadLocalUserSession.class);
-  private UserIdentityAuthenticator userIdentityAuthenticator = mock(UserIdentityAuthenticator.class);
+  private TestUserIdentityAuthenticator userIdentityAuthenticator = new TestUserIdentityAuthenticator();
   private Server server = mock(Server.class);
   private OAuthCsrfVerifier csrfVerifier = mock(OAuthCsrfVerifier.class);
   private JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class);
@@ -129,27 +125,62 @@ public class OAuth2ContextFactoryTest {
 
   @Test
   public void authenticate() {
-    UserDto userDto = dbTester.users().insertUser();
-    when(userIdentityAuthenticator.authenticate(USER_IDENTITY, identityProvider, Source.oauth2(identityProvider), WARN)).thenReturn(userDto);
     OAuth2IdentityProvider.CallbackContext callback = newCallbackContext();
 
     callback.authenticate(USER_IDENTITY);
 
-    verify(userIdentityAuthenticator).authenticate(USER_IDENTITY, identityProvider, Source.oauth2(identityProvider), WARN);
-    verify(jwtHttpHandler).generateToken(any(UserDto.class), eq(request), eq(response));
+    assertThat(userIdentityAuthenticator.isAuthenticated()).isTrue();
     verify(threadLocalUserSession).set(any(UserSession.class));
+    ArgumentCaptor<UserDto> userArgumentCaptor = ArgumentCaptor.forClass(UserDto.class);
+    verify(jwtHttpHandler).generateToken(userArgumentCaptor.capture(), eq(request), eq(response));
+    assertThat(userArgumentCaptor.getValue().getLogin()).isEqualTo(USER_IDENTITY.getLogin());
+    assertThat(userArgumentCaptor.getValue().getExternalId()).isEqualTo(USER_IDENTITY.getProviderId());
+    assertThat(userArgumentCaptor.getValue().getExternalLogin()).isEqualTo(USER_IDENTITY.getProviderLogin());
+    assertThat(userArgumentCaptor.getValue().getExternalIdentityProvider()).isEqualTo(PROVIDER_KEY);
   }
 
   @Test
   public void authenticate_with_allow_email_shift() {
     when(oAuthParameters.getAllowEmailShift(request)).thenReturn(Optional.of(true));
-    UserDto userDto = dbTester.users().insertUser();
-    when(userIdentityAuthenticator.authenticate(USER_IDENTITY, identityProvider, Source.oauth2(identityProvider), ALLOW)).thenReturn(userDto);
     OAuth2IdentityProvider.CallbackContext callback = newCallbackContext();
 
     callback.authenticate(USER_IDENTITY);
 
-    verify(userIdentityAuthenticator).authenticate(USER_IDENTITY, identityProvider, Source.oauth2(identityProvider), ALLOW);
+    assertThat(userIdentityAuthenticator.isAuthenticated()).isTrue();
+    assertThat(userIdentityAuthenticator.getAuthenticatorParameters().getExistingEmailStrategy()).isEqualTo(ExistingEmailStrategy.ALLOW);
+  }
+
+  @Test
+  public void authenticate_without_email_shift() {
+    when(oAuthParameters.getAllowEmailShift(request)).thenReturn(Optional.of(false));
+    OAuth2IdentityProvider.CallbackContext callback = newCallbackContext();
+
+    callback.authenticate(USER_IDENTITY);
+
+    assertThat(userIdentityAuthenticator.isAuthenticated()).isTrue();
+    assertThat(userIdentityAuthenticator.getAuthenticatorParameters().getExistingEmailStrategy()).isEqualTo(ExistingEmailStrategy.WARN);
+  }
+
+  @Test
+  public void authenticate_with_allow_login_update() {
+    when(oAuthParameters.getAllowUpdateLogin(request)).thenReturn(Optional.of(true));
+    OAuth2IdentityProvider.CallbackContext callback = newCallbackContext();
+
+    callback.authenticate(USER_IDENTITY);
+
+    assertThat(userIdentityAuthenticator.isAuthenticated()).isTrue();
+    assertThat(userIdentityAuthenticator.getAuthenticatorParameters().getUpdateLoginStrategy()).isEqualTo(UpdateLoginStrategy.ALLOW);
+  }
+
+  @Test
+  public void authenticate_without_allowing_login_update() {
+    when(oAuthParameters.getAllowUpdateLogin(request)).thenReturn(Optional.of(false));
+    OAuth2IdentityProvider.CallbackContext callback = newCallbackContext();
+
+    callback.authenticate(USER_IDENTITY);
+
+    assertThat(userIdentityAuthenticator.isAuthenticated()).isTrue();
+    assertThat(userIdentityAuthenticator.getAuthenticatorParameters().getUpdateLoginStrategy()).isEqualTo(UpdateLoginStrategy.WARN);
   }
 
   @Test
index e5b22162c1cbd228d8f8c1bc9e5c760de807d30d..a6211914ea9512e60f1de8622eae6ca077c1b7a2 100644 (file)
@@ -24,16 +24,12 @@ import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
-import org.mockito.ArgumentCaptor;
 import org.sonar.api.config.internal.MapSettings;
 import org.sonar.api.security.Authenticator;
 import org.sonar.api.security.ExternalGroupsProvider;
 import org.sonar.api.security.ExternalUsersProvider;
 import org.sonar.api.security.SecurityRealm;
 import org.sonar.api.security.UserDetails;
-import org.sonar.api.server.authentication.IdentityProvider;
-import org.sonar.api.server.authentication.UserIdentity;
-import org.sonar.db.user.UserDto;
 import org.sonar.server.authentication.event.AuthenticationEvent;
 import org.sonar.server.authentication.event.AuthenticationEvent.Source;
 import org.sonar.server.user.SecurityRealmFactory;
@@ -41,7 +37,6 @@ import org.sonar.server.user.SecurityRealmFactory;
 import static java.util.Arrays.asList;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.rules.ExpectedException.none;
-import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
@@ -49,8 +44,7 @@ import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
-import static org.sonar.db.user.UserTesting.newUserDto;
-import static org.sonar.server.authentication.UserIdentityAuthenticator.ExistingEmailStrategy.FORBID;
+import static org.sonar.server.authentication.UserIdentityAuthenticatorParameters.ExistingEmailStrategy.FORBID;
 import static org.sonar.server.authentication.event.AuthenticationEvent.Method.BASIC;
 import static org.sonar.server.authentication.event.AuthenticationEvent.Method.BASIC_TOKEN;
 import static org.sonar.server.authentication.event.AuthenticationExceptionMatcher.authenticationException;
@@ -60,16 +54,11 @@ public class RealmAuthenticatorTest {
   private static final String LOGIN = "LOGIN";
   private static final String PASSWORD = "PASSWORD";
 
-  private static final UserDto USER = newUserDto();
   private static final String REALM_NAME = "realm name";
 
   @Rule
   public ExpectedException expectedException = none();
 
-  private ArgumentCaptor<UserIdentity> userIdentityArgumentCaptor = ArgumentCaptor.forClass(UserIdentity.class);
-  private ArgumentCaptor<IdentityProvider> identityProviderArgumentCaptor = ArgumentCaptor.forClass(IdentityProvider.class);
-  private ArgumentCaptor<AuthenticationEvent.Source> sourceCaptor = ArgumentCaptor.forClass(Source.class);
-
   private MapSettings settings = new MapSettings();
 
   private SecurityRealmFactory securityRealmFactory = mock(SecurityRealmFactory.class);
@@ -78,7 +67,7 @@ public class RealmAuthenticatorTest {
   private ExternalUsersProvider externalUsersProvider = mock(ExternalUsersProvider.class);
   private ExternalGroupsProvider externalGroupsProvider = mock(ExternalGroupsProvider.class);
 
-  private UserIdentityAuthenticator userIdentityAuthenticator = mock(UserIdentityAuthenticator.class);
+  private TestUserIdentityAuthenticator userIdentityAuthenticator = new TestUserIdentityAuthenticator();
   private AuthenticationEvent authenticationEvent = mock(AuthenticationEvent.class);
 
   private HttpServletRequest request = mock(HttpServletRequest.class);
@@ -98,17 +87,17 @@ public class RealmAuthenticatorTest {
     userDetails.setName("name");
     userDetails.setEmail("email");
     when(externalUsersProvider.doGetUserDetails(any(ExternalUsersProvider.Context.class))).thenReturn(userDetails);
-    when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class), any(Source.class), eq(FORBID))).thenReturn(USER);
 
     underTest.authenticate(LOGIN, PASSWORD, request, BASIC);
 
-    verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture(), sourceCaptor.capture(), eq(FORBID));
-    UserIdentity userIdentity = userIdentityArgumentCaptor.getValue();
-    assertThat(userIdentity.getLogin()).isEqualTo(LOGIN);
-    assertThat(userIdentity.getProviderLogin()).isEqualTo(LOGIN);
-    assertThat(userIdentity.getName()).isEqualTo("name");
-    assertThat(userIdentity.getEmail()).isEqualTo("email");
-    assertThat(userIdentity.shouldSyncGroups()).isFalse();
+    assertThat(userIdentityAuthenticator.isAuthenticated()).isTrue();
+    assertThat(userIdentityAuthenticator.getAuthenticatorParameters().getExistingEmailStrategy()).isEqualTo(FORBID);
+    assertThat(userIdentityAuthenticator.getAuthenticatorParameters().getUserIdentity().getLogin()).isEqualTo(LOGIN);
+    assertThat(userIdentityAuthenticator.getAuthenticatorParameters().getUserIdentity().getProviderLogin()).isEqualTo(LOGIN);
+    assertThat(userIdentityAuthenticator.getAuthenticatorParameters().getUserIdentity().getProviderId()).isNull();
+    assertThat(userIdentityAuthenticator.getAuthenticatorParameters().getUserIdentity().getName()).isEqualTo("name");
+    assertThat(userIdentityAuthenticator.getAuthenticatorParameters().getUserIdentity().getEmail()).isEqualTo("email");
+    assertThat(userIdentityAuthenticator.getAuthenticatorParameters().getUserIdentity().shouldSyncGroups()).isFalse();
     verify(authenticationEvent).loginSuccess(request, LOGIN, Source.realm(BASIC, REALM_NAME));
   }
 
@@ -120,16 +109,14 @@ public class RealmAuthenticatorTest {
     userDetails.setName("name");
     userDetails.setEmail("email");
     when(externalUsersProvider.doGetUserDetails(any(ExternalUsersProvider.Context.class))).thenReturn(userDetails);
-    when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class), any(Source.class), eq(FORBID))).thenReturn(USER);
 
     underTest.authenticate(LOGIN, PASSWORD, request, BASIC);
 
-    verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture(), sourceCaptor.capture(), eq(FORBID));
-
-    assertThat(identityProviderArgumentCaptor.getValue().getKey()).isEqualTo("sonarqube");
-    assertThat(identityProviderArgumentCaptor.getValue().getName()).isEqualTo("sonarqube");
-    assertThat(identityProviderArgumentCaptor.getValue().getDisplay()).isNull();
-    assertThat(identityProviderArgumentCaptor.getValue().isEnabled()).isTrue();
+    assertThat(userIdentityAuthenticator.isAuthenticated()).isTrue();
+    assertThat(userIdentityAuthenticator.getAuthenticatorParameters().getProvider().getKey()).isEqualTo("sonarqube");
+    assertThat(userIdentityAuthenticator.getAuthenticatorParameters().getProvider().getName()).isEqualTo("sonarqube");
+    assertThat(userIdentityAuthenticator.getAuthenticatorParameters().getProvider().getDisplay()).isNull();
+    assertThat(userIdentityAuthenticator.getAuthenticatorParameters().getProvider().isEnabled()).isTrue();
     verify(authenticationEvent).loginSuccess(request, LOGIN, Source.realm(BASIC, REALM_NAME));
   }
 
@@ -140,27 +127,22 @@ public class RealmAuthenticatorTest {
     UserDetails userDetails = new UserDetails();
     userDetails.setEmail("email");
     when(externalUsersProvider.doGetUserDetails(any(ExternalUsersProvider.Context.class))).thenReturn(userDetails);
-    when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class), any(Source.class), eq(FORBID))).thenReturn(USER);
 
     underTest.authenticate(LOGIN, PASSWORD, request, BASIC);
 
-    verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture(), sourceCaptor.capture(), eq(FORBID));
-    assertThat(identityProviderArgumentCaptor.getValue().getName()).isEqualTo("sonarqube");
+    assertThat(userIdentityAuthenticator.getAuthenticatorParameters().getProvider().getName()).isEqualTo("sonarqube");
     verify(authenticationEvent).loginSuccess(request, LOGIN, Source.realm(BASIC, REALM_NAME));
   }
 
   @Test
   public void authenticate_with_group_sync() {
     when(externalGroupsProvider.doGetGroups(any(ExternalGroupsProvider.Context.class))).thenReturn(asList("group1", "group2"));
-    when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class), any(Source.class), eq(FORBID))).thenReturn(USER);
     executeStartWithGroupSync();
-    executeAuthenticate();
 
-    verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture(), sourceCaptor.capture(), eq(FORBID));
+    executeAuthenticate();
 
-    UserIdentity userIdentity = userIdentityArgumentCaptor.getValue();
-    assertThat(userIdentity.shouldSyncGroups()).isTrue();
-    assertThat(userIdentity.getGroups()).containsOnly("group1", "group2");
+    assertThat(userIdentityAuthenticator.isAuthenticated()).isTrue();
+    assertThat(userIdentityAuthenticator.getAuthenticatorParameters().getUserIdentity().shouldSyncGroups()).isTrue();
     verify(authenticationEvent).loginSuccess(request, LOGIN, Source.realm(BASIC, REALM_NAME));
   }
 
@@ -171,40 +153,37 @@ public class RealmAuthenticatorTest {
     UserDetails userDetails = new UserDetails();
     userDetails.setName(null);
     when(externalUsersProvider.doGetUserDetails(any(ExternalUsersProvider.Context.class))).thenReturn(userDetails);
-    when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class), any(Source.class), eq(FORBID))).thenReturn(USER);
 
     underTest.authenticate(LOGIN, PASSWORD, request, BASIC);
 
-    verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture(), sourceCaptor.capture(), eq(FORBID));
-    assertThat(userIdentityArgumentCaptor.getValue().getName()).isEqualTo(LOGIN);
+    assertThat(userIdentityAuthenticator.isAuthenticated()).isTrue();
+    assertThat(userIdentityAuthenticator.getAuthenticatorParameters().getUserIdentity().getName()).isEqualTo(LOGIN);
     verify(authenticationEvent).loginSuccess(request, LOGIN, Source.realm(BASIC, REALM_NAME));
   }
 
   @Test
   public void use_downcase_login() {
     settings.setProperty("sonar.authenticator.downcase", true);
-    when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class), any(Source.class), eq(FORBID))).thenReturn(USER);
     executeStartWithoutGroupSync();
+
     executeAuthenticate("LOGIN");
 
-    verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture(), sourceCaptor.capture(), eq(FORBID));
-    UserIdentity userIdentity = userIdentityArgumentCaptor.getValue();
-    assertThat(userIdentity.getLogin()).isEqualTo("login");
-    assertThat(userIdentity.getProviderLogin()).isEqualTo("login");
+    assertThat(userIdentityAuthenticator.isAuthenticated()).isTrue();
+    assertThat(userIdentityAuthenticator.getAuthenticatorParameters().getUserIdentity().getLogin()).isEqualTo("login");
+    assertThat(userIdentityAuthenticator.getAuthenticatorParameters().getUserIdentity().getProviderLogin()).isEqualTo("login");
     verify(authenticationEvent).loginSuccess(request, "login", Source.realm(BASIC, REALM_NAME));
   }
 
   @Test
   public void does_not_user_downcase_login() {
     settings.setProperty("sonar.authenticator.downcase", false);
-    when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class), any(Source.class), eq(FORBID))).thenReturn(USER);
     executeStartWithoutGroupSync();
+
     executeAuthenticate("LoGiN");
 
-    verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture(), sourceCaptor.capture(), eq(FORBID));
-    UserIdentity userIdentity = userIdentityArgumentCaptor.getValue();
-    assertThat(userIdentity.getLogin()).isEqualTo("LoGiN");
-    assertThat(userIdentity.getProviderLogin()).isEqualTo("LoGiN");
+    assertThat(userIdentityAuthenticator.isAuthenticated()).isTrue();
+    assertThat(userIdentityAuthenticator.getAuthenticatorParameters().getUserIdentity().getLogin()).isEqualTo("LoGiN");
+    assertThat(userIdentityAuthenticator.getAuthenticatorParameters().getUserIdentity().getProviderLogin()).isEqualTo("LoGiN");
     verify(authenticationEvent).loginSuccess(request, "LoGiN", Source.realm(BASIC, REALM_NAME));
   }
 
index 553935c8aece712278ea09a07ecce281d808084a..98d8b00a15cc374ef76278a613364e90c46540f7 100644 (file)
@@ -42,7 +42,7 @@ import org.sonar.server.authentication.event.AuthenticationEvent;
 import org.sonar.server.authentication.event.AuthenticationEvent.Source;
 import org.sonar.server.es.EsTester;
 import org.sonar.server.organization.DefaultOrganizationProvider;
-import org.sonar.server.organization.OrganizationCreation;
+import org.sonar.server.organization.OrganizationUpdater;
 import org.sonar.server.organization.TestDefaultOrganizationProvider;
 import org.sonar.server.organization.TestOrganizationFlags;
 import org.sonar.server.user.NewUserNotifier;
@@ -97,17 +97,17 @@ public class SsoAuthenticatorTest {
   private GroupDto sonarUsers;
 
   private System2 system2 = mock(System2.class);
-  private OrganizationCreation organizationCreation = mock(OrganizationCreation.class);
+  private OrganizationUpdater organizationUpdater = mock(OrganizationUpdater.class);
   private DefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(db);
   private TestOrganizationFlags organizationFlags = TestOrganizationFlags.standalone();
   private LocalAuthentication localAuthentication = new LocalAuthentication(db.getDbClient());
 
   private UserIndexer userIndexer = new UserIndexer(db.getDbClient(), es.client());
-  private UserIdentityAuthenticator userIdentityAuthenticator = new UserIdentityAuthenticator(
+  private UserIdentityAuthenticatorImpl userIdentityAuthenticator = new UserIdentityAuthenticatorImpl(
     db.getDbClient(),
-    new UserUpdater(mock(NewUserNotifier.class), db.getDbClient(), userIndexer, organizationFlags, defaultOrganizationProvider, organizationCreation,
+    new UserUpdater(mock(NewUserNotifier.class), db.getDbClient(), userIndexer, organizationFlags, defaultOrganizationProvider, organizationUpdater,
       new DefaultGroupFinder(db.getDbClient()), settings.asConfig(), localAuthentication),
-    defaultOrganizationProvider, organizationFlags, new DefaultGroupFinder(db.getDbClient()));
+    defaultOrganizationProvider, organizationFlags, mock(OrganizationUpdater.class), new DefaultGroupFinder(db.getDbClient()));
 
   private HttpServletResponse response = mock(HttpServletResponse.class);
   private JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class);
diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/TestUserIdentityAuthenticator.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/TestUserIdentityAuthenticator.java
new file mode 100644 (file)
index 0000000..bfb42c3
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.authentication;
+
+import org.sonar.db.user.UserDto;
+import org.sonar.db.user.UserTesting;
+
+public class TestUserIdentityAuthenticator implements UserIdentityAuthenticator {
+
+  private UserIdentityAuthenticatorParameters authenticatorParameters;
+
+  @Override
+  public UserDto authenticate(UserIdentityAuthenticatorParameters authenticatorParameters) {
+    this.authenticatorParameters = authenticatorParameters;
+    String providerId = authenticatorParameters.getUserIdentity().getProviderId();
+    return UserTesting.newUserDto()
+      .setLocal(false)
+      .setLogin(authenticatorParameters.getUserIdentity().getLogin())
+      .setExternalLogin(authenticatorParameters.getUserIdentity().getProviderLogin())
+      .setExternalId(providerId == null ? authenticatorParameters.getUserIdentity().getProviderLogin() : providerId)
+      .setExternalIdentityProvider(authenticatorParameters.getProvider().getKey());
+  }
+
+  boolean isAuthenticated() {
+    return authenticatorParameters != null;
+  }
+
+  UserIdentityAuthenticatorParameters getAuthenticatorParameters() {
+    return authenticatorParameters;
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/UserIdentityAuthenticatorImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/UserIdentityAuthenticatorImplTest.java
new file mode 100644 (file)
index 0000000..fe778cb
--- /dev/null
@@ -0,0 +1,786 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.authentication;
+
+import java.util.Optional;
+import java.util.stream.Collectors;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.api.server.authentication.UserIdentity;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.internal.AlwaysIncreasingSystem2;
+import org.sonar.core.util.UuidFactoryFast;
+import org.sonar.core.util.stream.MoreCollectors;
+import org.sonar.db.DbTester;
+import org.sonar.db.organization.OrganizationDto;
+import org.sonar.db.user.GroupDto;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.authentication.UserIdentityAuthenticatorParameters.ExistingEmailStrategy;
+import org.sonar.server.authentication.UserIdentityAuthenticatorParameters.UpdateLoginStrategy;
+import org.sonar.server.authentication.event.AuthenticationEvent;
+import org.sonar.server.authentication.event.AuthenticationEvent.Source;
+import org.sonar.server.authentication.exception.EmailAlreadyExistsRedirectionException;
+import org.sonar.server.authentication.exception.UpdateLoginRedirectionException;
+import org.sonar.server.es.EsTester;
+import org.sonar.server.organization.DefaultOrganizationProvider;
+import org.sonar.server.organization.OrganizationUpdater;
+import org.sonar.server.organization.OrganizationUpdaterImpl;
+import org.sonar.server.organization.OrganizationValidationImpl;
+import org.sonar.server.organization.TestDefaultOrganizationProvider;
+import org.sonar.server.organization.TestOrganizationFlags;
+import org.sonar.server.user.NewUserNotifier;
+import org.sonar.server.user.UserUpdater;
+import org.sonar.server.user.index.UserIndexer;
+import org.sonar.server.usergroups.DefaultGroupFinder;
+
+import static com.google.common.collect.Sets.newHashSet;
+import static java.util.Arrays.stream;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.sonar.core.config.CorePropertyDefinitions.ONBOARDING_TUTORIAL_SHOW_TO_NEW_USERS;
+import static org.sonar.db.user.UserTesting.newUserDto;
+import static org.sonar.server.authentication.UserIdentityAuthenticatorParameters.ExistingEmailStrategy.FORBID;
+import static org.sonar.server.authentication.event.AuthenticationEvent.Method.BASIC;
+import static org.sonar.server.authentication.event.AuthenticationExceptionMatcher.authenticationException;
+
+public class UserIdentityAuthenticatorImplTest {
+
+  private static String USER_LOGIN = "github-johndoo";
+
+  private static UserIdentity USER_IDENTITY = UserIdentity.builder()
+    .setProviderId("ABCD")
+    .setProviderLogin("johndoo")
+    .setLogin(USER_LOGIN)
+    .setName("John")
+    .setEmail("john@email.com")
+    .build();
+
+  private static TestIdentityProvider IDENTITY_PROVIDER = new TestIdentityProvider()
+    .setKey("github")
+    .setName("name of github")
+    .setEnabled(true)
+    .setAllowsUsersToSignUp(true);
+
+  private MapSettings settings = new MapSettings();
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+  @Rule
+  public DbTester db = DbTester.create(new AlwaysIncreasingSystem2());
+  @Rule
+  public EsTester es = EsTester.create();
+  private UserIndexer userIndexer = new UserIndexer(db.getDbClient(), es.client());
+  private DefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(db);
+  private OrganizationUpdater organizationUpdater = mock(OrganizationUpdater.class);
+  private TestOrganizationFlags organizationFlags = TestOrganizationFlags.standalone();
+  private LocalAuthentication localAuthentication = new LocalAuthentication(db.getDbClient());
+  private UserUpdater userUpdater = new UserUpdater(
+    mock(NewUserNotifier.class),
+    db.getDbClient(),
+    userIndexer,
+    organizationFlags,
+    defaultOrganizationProvider,
+    organizationUpdater,
+    new DefaultGroupFinder(db.getDbClient()),
+    settings.asConfig(),
+    localAuthentication);
+
+  private UserIdentityAuthenticatorImpl underTest = new UserIdentityAuthenticatorImpl(db.getDbClient(), userUpdater, defaultOrganizationProvider, organizationFlags,
+    new OrganizationUpdaterImpl(db.getDbClient(), mock(System2.class), UuidFactoryFast.getInstance(),
+      new OrganizationValidationImpl(), settings.asConfig(), null, null, null),
+    new DefaultGroupFinder(db.getDbClient()));
+
+  @Test
+  public void authenticate_new_user() {
+    organizationFlags.setEnabled(true);
+
+    underTest.authenticate(UserIdentityAuthenticatorParameters.builder()
+      .setUserIdentity(USER_IDENTITY)
+      .setProvider(IDENTITY_PROVIDER)
+      .setSource(Source.realm(BASIC, IDENTITY_PROVIDER.getName()))
+      .setExistingEmailStrategy(ExistingEmailStrategy.FORBID)
+      .setUpdateLoginStrategy(UpdateLoginStrategy.ALLOW)
+      .build());
+
+    UserDto user = db.users().selectUserByLogin(USER_LOGIN).get();
+    assertThat(user).isNotNull();
+    assertThat(user.isActive()).isTrue();
+    assertThat(user.getName()).isEqualTo("John");
+    assertThat(user.getEmail()).isEqualTo("john@email.com");
+    assertThat(user.getExternalLogin()).isEqualTo("johndoo");
+    assertThat(user.getExternalIdentityProvider()).isEqualTo("github");
+    assertThat(user.getExternalId()).isEqualTo("ABCD");
+    assertThat(user.isRoot()).isFalse();
+    checkGroupMembership(user);
+  }
+
+  @Test
+  public void authenticate_new_user_with_groups() {
+    organizationFlags.setEnabled(true);
+    GroupDto group1 = db.users().insertGroup(db.getDefaultOrganization(), "group1");
+    GroupDto group2 = db.users().insertGroup(db.getDefaultOrganization(), "group2");
+
+    authenticate(USER_LOGIN, "group1", "group2", "group3");
+
+    Optional<UserDto> user = db.users().selectUserByLogin(USER_LOGIN);
+    checkGroupMembership(user.get(), group1, group2);
+  }
+
+  @Test
+  public void authenticate_new_user_and_force_default_group_when_organizations_are_disabled() {
+    organizationFlags.setEnabled(false);
+    UserDto user = db.users().insertUser();
+    GroupDto group1 = db.users().insertGroup(db.getDefaultOrganization(), "group1");
+    GroupDto defaultGroup = insertDefaultGroup();
+    db.users().insertMember(group1, user);
+    db.users().insertMember(defaultGroup, user);
+
+    authenticate(user.getLogin(), "group1");
+
+    checkGroupMembership(user, group1, defaultGroup);
+  }
+
+  @Test
+  public void does_not_force_default_group_when_authenticating_new_user_if_organizations_are_enabled() {
+    organizationFlags.setEnabled(true);
+    UserDto user = db.users().insertUser();
+    GroupDto group1 = db.users().insertGroup(db.getDefaultOrganization(), "group1");
+    GroupDto defaultGroup = insertDefaultGroup();
+    db.users().insertMember(group1, user);
+    db.users().insertMember(defaultGroup, user);
+
+    authenticate(user.getLogin(), "group1");
+
+    checkGroupMembership(user, group1);
+  }
+
+  @Test
+  public void authenticate_new_user_sets_onboarded_flag_to_false_when_onboarding_setting_is_set_to_true() {
+    organizationFlags.setEnabled(true);
+    settings.setProperty(ONBOARDING_TUTORIAL_SHOW_TO_NEW_USERS, true);
+
+    underTest.authenticate(UserIdentityAuthenticatorParameters.builder()
+      .setUserIdentity(USER_IDENTITY)
+      .setProvider(IDENTITY_PROVIDER)
+      .setSource(Source.local(BASIC))
+      .setExistingEmailStrategy(ExistingEmailStrategy.FORBID)
+      .setUpdateLoginStrategy(UpdateLoginStrategy.ALLOW)
+      .build());
+
+    assertThat(db.users().selectUserByLogin(USER_LOGIN).get().isOnboarded()).isFalse();
+  }
+
+  @Test
+  public void authenticate_new_user_sets_onboarded_flag_to_true_when_onboarding_setting_is_set_to_false() {
+    organizationFlags.setEnabled(true);
+    settings.setProperty(ONBOARDING_TUTORIAL_SHOW_TO_NEW_USERS, false);
+
+    underTest.authenticate(UserIdentityAuthenticatorParameters.builder()
+      .setUserIdentity(USER_IDENTITY)
+      .setProvider(IDENTITY_PROVIDER)
+      .setSource(Source.local(BASIC))
+      .setExistingEmailStrategy(ExistingEmailStrategy.FORBID)
+      .setUpdateLoginStrategy(UpdateLoginStrategy.ALLOW)
+      .build());
+
+    assertThat(db.users().selectUserByLogin(USER_LOGIN).get().isOnboarded()).isTrue();
+  }
+
+  @Test
+  public void external_id_is_set_to_provider_login_when_null() {
+    organizationFlags.setEnabled(true);
+    UserIdentity newUser = UserIdentity.builder()
+      .setProviderId(null)
+      .setLogin("john")
+      .setProviderLogin("johndoo")
+      .setName("JOhn")
+      .build();
+
+    underTest.authenticate(UserIdentityAuthenticatorParameters.builder()
+      .setUserIdentity(newUser)
+      .setProvider(IDENTITY_PROVIDER)
+      .setSource(Source.local(BASIC))
+      .setExistingEmailStrategy(ExistingEmailStrategy.FORBID)
+      .setUpdateLoginStrategy(UpdateLoginStrategy.ALLOW)
+      .build());
+
+    assertThat(db.users().selectUserByLogin(newUser.getLogin()).get())
+      .extracting(UserDto::getLogin, UserDto::getExternalId, UserDto::getExternalLogin)
+      .contains("john", "johndoo", "johndoo");
+  }
+
+  @Test
+  public void authenticate_new_user_update_existing_user_email_when_strategy_is_ALLOW() {
+    organizationFlags.setEnabled(true);
+    UserDto existingUser = db.users().insertUser(u -> u.setEmail("john@email.com"));
+    UserIdentity newUser = UserIdentity.builder()
+      .setProviderLogin("johndoo")
+      .setLogin("new_login")
+      .setName(existingUser.getName())
+      .setEmail(existingUser.getEmail())
+      .build();
+
+    underTest.authenticate(UserIdentityAuthenticatorParameters.builder()
+      .setUserIdentity(newUser)
+      .setProvider(IDENTITY_PROVIDER)
+      .setSource(Source.local(BASIC))
+      .setExistingEmailStrategy(ExistingEmailStrategy.ALLOW)
+      .setUpdateLoginStrategy(UpdateLoginStrategy.ALLOW)
+      .build());
+
+    UserDto newUserReloaded = db.users().selectUserByLogin(newUser.getLogin()).get();
+    assertThat(newUserReloaded.getEmail()).isEqualTo(existingUser.getEmail());
+    UserDto existingUserReloaded = db.users().selectUserByLogin(existingUser.getLogin()).get();
+    assertThat(existingUserReloaded.getEmail()).isNull();
+  }
+
+  @Test
+  public void throw_EmailAlreadyExistException_when_authenticating_new_user_when_email_already_exists_and_strategy_is_WARN() {
+    organizationFlags.setEnabled(true);
+    UserDto existingUser = db.users().insertUser(u -> u.setEmail("john@email.com"));
+    UserIdentity newUser = UserIdentity.builder()
+      .setProviderLogin("johndoo")
+      .setLogin("new_login")
+      .setName(existingUser.getName())
+      .setEmail(existingUser.getEmail())
+      .build();
+
+    expectedException.expect(EmailAlreadyExistsRedirectionException.class);
+
+    underTest.authenticate(UserIdentityAuthenticatorParameters.builder()
+      .setUserIdentity(newUser)
+      .setProvider(IDENTITY_PROVIDER)
+      .setSource(Source.local(BASIC))
+      .setExistingEmailStrategy(ExistingEmailStrategy.WARN)
+      .setUpdateLoginStrategy(UpdateLoginStrategy.ALLOW)
+      .build());
+  }
+
+  @Test
+  public void throw_AuthenticationException_when_authenticating_new_user_when_email_already_exists_and_strategy_is_FORBID() {
+    db.users().insertUser(newUserDto()
+      .setLogin("Existing user with same email")
+      .setActive(true)
+      .setEmail("john@email.com"));
+    Source source = Source.realm(AuthenticationEvent.Method.FORM, IDENTITY_PROVIDER.getName());
+
+    expectedException.expect(authenticationException().from(source)
+      .withLogin(USER_IDENTITY.getLogin())
+      .andPublicMessage("You can't sign up because email 'john@email.com' is already used by an existing user. " +
+        "This means that you probably already registered with another account."));
+    expectedException.expectMessage("Email 'john@email.com' is already used");
+
+    underTest.authenticate(UserIdentityAuthenticatorParameters.builder()
+      .setUserIdentity(USER_IDENTITY)
+      .setProvider(IDENTITY_PROVIDER)
+      .setSource(source)
+      .setExistingEmailStrategy(FORBID)
+      .setExistingEmailStrategy(ExistingEmailStrategy.FORBID)
+      .setUpdateLoginStrategy(UpdateLoginStrategy.ALLOW)
+      .build());
+  }
+
+  @Test
+  public void fail_to_authenticate_new_user_when_allow_users_to_signup_is_false() {
+    TestIdentityProvider identityProvider = new TestIdentityProvider()
+      .setKey("github")
+      .setName("Github")
+      .setEnabled(true)
+      .setAllowsUsersToSignUp(false);
+    Source source = Source.realm(AuthenticationEvent.Method.FORM, identityProvider.getName());
+
+    expectedException.expect(authenticationException().from(source).withLogin(USER_IDENTITY.getLogin()).andPublicMessage("'github' users are not allowed to sign up"));
+    expectedException.expectMessage("User signup disabled for provider 'github'");
+
+    underTest.authenticate(UserIdentityAuthenticatorParameters.builder()
+      .setUserIdentity(USER_IDENTITY)
+      .setProvider(identityProvider)
+      .setSource(source)
+      .setExistingEmailStrategy(ExistingEmailStrategy.FORBID)
+      .setUpdateLoginStrategy(UpdateLoginStrategy.ALLOW)
+      .build());
+  }
+
+  @Test
+  public void authenticate_existing_user_matching_login() {
+    db.users().insertUser(u -> u
+      .setLogin(USER_LOGIN)
+      .setName("Old name")
+      .setEmail("Old email")
+      .setExternalId("old id")
+      .setExternalLogin("old identity")
+      .setExternalIdentityProvider("old provide"));
+
+    underTest.authenticate(UserIdentityAuthenticatorParameters.builder()
+      .setUserIdentity(USER_IDENTITY)
+      .setProvider(IDENTITY_PROVIDER)
+      .setSource(Source.local(BASIC))
+      .setExistingEmailStrategy(ExistingEmailStrategy.FORBID)
+      .setUpdateLoginStrategy(UpdateLoginStrategy.ALLOW)
+      .build());
+
+    assertThat(db.users().selectUserByLogin(USER_LOGIN).get())
+      .extracting(UserDto::getName, UserDto::getEmail, UserDto::getExternalId, UserDto::getExternalLogin, UserDto::getExternalIdentityProvider, UserDto::isActive)
+      .contains("John", "john@email.com", "ABCD", "johndoo", "github", true);
+  }
+
+  @Test
+  public void authenticate_existing_user_matching_external_id() {
+    UserDto user = db.users().insertUser(u -> u
+      .setLogin("Old login")
+      .setName("Old name")
+      .setEmail("Old email")
+      .setExternalId(USER_IDENTITY.getProviderId())
+      .setExternalLogin("old identity")
+      .setExternalIdentityProvider(IDENTITY_PROVIDER.getKey()));
+
+    underTest.authenticate(UserIdentityAuthenticatorParameters.builder()
+      .setUserIdentity(USER_IDENTITY)
+      .setProvider(IDENTITY_PROVIDER)
+      .setSource(Source.local(BASIC))
+      .setExistingEmailStrategy(ExistingEmailStrategy.FORBID)
+      .setUpdateLoginStrategy(UpdateLoginStrategy.ALLOW)
+      .build());
+
+    assertThat(db.users().selectUserByLogin("Old login")).isNotPresent();
+    assertThat(db.getDbClient().userDao().selectByUuid(db.getSession(), user.getUuid()))
+      .extracting(UserDto::getLogin, UserDto::getName, UserDto::getEmail, UserDto::getExternalId, UserDto::getExternalLogin, UserDto::getExternalIdentityProvider,
+        UserDto::isActive)
+      .contains(USER_LOGIN, "John", "john@email.com", "ABCD", "johndoo", "github", true);
+  }
+
+  @Test
+  public void authenticate_existing_user_and_update_only_login() {
+    UserDto user = db.users().insertUser(u -> u
+      .setLogin("old login")
+      .setName(USER_IDENTITY.getName())
+      .setEmail(USER_IDENTITY.getEmail())
+      .setExternalId(USER_IDENTITY.getProviderId())
+      .setExternalLogin("old identity")
+      .setExternalIdentityProvider(IDENTITY_PROVIDER.getKey()));
+
+    underTest.authenticate(UserIdentityAuthenticatorParameters.builder()
+      .setUserIdentity(USER_IDENTITY)
+      .setProvider(IDENTITY_PROVIDER)
+      .setSource(Source.local(BASIC))
+      .setExistingEmailStrategy(ExistingEmailStrategy.FORBID)
+      .setUpdateLoginStrategy(UpdateLoginStrategy.ALLOW)
+      .build());
+
+    assertThat(db.users().selectUserByLogin("Old login")).isNotPresent();
+    assertThat(db.getDbClient().userDao().selectByUuid(db.getSession(), user.getUuid()))
+      .extracting(UserDto::getLogin, UserDto::getName, UserDto::getEmail, UserDto::getExternalId, UserDto::getExternalLogin, UserDto::getExternalIdentityProvider,
+        UserDto::isActive)
+      .containsExactlyInAnyOrder(USER_LOGIN, USER_IDENTITY.getName(), USER_IDENTITY.getEmail(), USER_IDENTITY.getProviderId(), USER_IDENTITY.getProviderLogin(),
+        IDENTITY_PROVIDER.getKey(),
+        true);
+  }
+
+  @Test
+  public void authenticate_existing_user_matching_login_when_external_id_is_null() {
+    UserDto user = db.users().insertUser(u -> u
+      .setLogin(USER_LOGIN)
+      .setName("Old name")
+      .setEmail("Old email")
+      .setExternalId("Old id")
+      .setExternalLogin("old identity")
+      .setExternalIdentityProvider(IDENTITY_PROVIDER.getKey()));
+
+    underTest.authenticate(UserIdentityAuthenticatorParameters.builder()
+      .setUserIdentity(UserIdentity.builder()
+        .setProviderId(null)
+        .setProviderLogin("johndoo")
+        .setLogin(USER_LOGIN)
+        .setName("John")
+        .setEmail("john@email.com")
+        .build())
+      .setProvider(IDENTITY_PROVIDER)
+      .setSource(Source.local(BASIC))
+      .setExistingEmailStrategy(ExistingEmailStrategy.FORBID)
+      .setUpdateLoginStrategy(UpdateLoginStrategy.ALLOW)
+      .build());
+
+    assertThat(db.getDbClient().userDao().selectByUuid(db.getSession(), user.getUuid()))
+      .extracting(UserDto::getLogin, UserDto::getName, UserDto::getEmail, UserDto::getExternalId, UserDto::getExternalLogin, UserDto::getExternalIdentityProvider,
+        UserDto::isActive)
+      .contains(user.getLogin(), "John", "john@email.com", "johndoo", "johndoo", "github", true);
+  }
+
+  @Test
+  public void authenticate_existing_user_with_login_update_and_strategy_is_ALLOW() {
+    UserDto user = db.users().insertUser(u -> u
+      .setLogin("Old login")
+      .setExternalId(USER_IDENTITY.getProviderId())
+      .setExternalLogin("old identity")
+      .setExternalIdentityProvider(IDENTITY_PROVIDER.getKey()));
+
+    underTest.authenticate(UserIdentityAuthenticatorParameters.builder()
+      .setUserIdentity(USER_IDENTITY)
+      .setProvider(IDENTITY_PROVIDER)
+      .setSource(Source.local(BASIC))
+      .setExistingEmailStrategy(ExistingEmailStrategy.FORBID)
+      .setUpdateLoginStrategy(UpdateLoginStrategy.ALLOW)
+      .build());
+
+    assertThat(db.getDbClient().userDao().selectByUuid(db.getSession(), user.getUuid()))
+      .extracting(UserDto::getLogin, UserDto::getExternalLogin)
+      .contains(USER_LOGIN, USER_IDENTITY.getProviderLogin());
+  }
+
+  @Test
+  public void authenticate_existing_user_with_login_update_and_personal_org_does_not_exits_and_strategy_is_WARN() {
+    organizationFlags.setEnabled(true);
+    UserDto user = db.users().insertUser(u -> u
+      .setLogin("Old login")
+      .setExternalId(USER_IDENTITY.getProviderId())
+      .setExternalLogin("old identity")
+      .setExternalIdentityProvider(IDENTITY_PROVIDER.getKey())
+      .setOrganizationUuid(null));
+
+    underTest.authenticate(UserIdentityAuthenticatorParameters.builder()
+      .setUserIdentity(USER_IDENTITY)
+      .setProvider(IDENTITY_PROVIDER)
+      .setSource(Source.local(BASIC))
+      .setExistingEmailStrategy(ExistingEmailStrategy.FORBID)
+      .setUpdateLoginStrategy(UpdateLoginStrategy.WARN)
+      .build());
+
+    assertThat(db.getDbClient().userDao().selectByUuid(db.getSession(), user.getUuid()))
+      .extracting(UserDto::getLogin, UserDto::getExternalLogin)
+      .contains(USER_LOGIN, USER_IDENTITY.getProviderLogin());
+  }
+
+  @Test
+  public void throw_UpdateLoginRedirectionException_when_authenticating_with_login_update_and_personal_org_exists_and_strategy_is_WARN() {
+    organizationFlags.setEnabled(true);
+    OrganizationDto organization = db.organizations().insert(o -> o.setKey("Old login"));
+    db.users().insertUser(u -> u
+      .setLogin("Old login")
+      .setExternalId(USER_IDENTITY.getProviderId())
+      .setExternalLogin("old identity")
+      .setExternalIdentityProvider(IDENTITY_PROVIDER.getKey())
+      .setOrganizationUuid(organization.getUuid()));
+
+    expectedException.expect(UpdateLoginRedirectionException.class);
+
+    underTest.authenticate(UserIdentityAuthenticatorParameters.builder()
+      .setUserIdentity(USER_IDENTITY)
+      .setProvider(IDENTITY_PROVIDER)
+      .setSource(Source.local(BASIC))
+      .setExistingEmailStrategy(ExistingEmailStrategy.FORBID)
+      .setUpdateLoginStrategy(UpdateLoginStrategy.WARN)
+      .build());
+  }
+
+  @Test
+  public void authenticate_existing_user_and_update_personal_og_key_when_personal_org_exists_and_strategy_is_ALLOW() {
+    organizationFlags.setEnabled(true);
+    OrganizationDto personalOrganization = db.organizations().insert(o -> o.setKey("Old login"));
+    UserDto user = db.users().insertUser(u -> u
+      .setLogin("Old login")
+      .setExternalId(USER_IDENTITY.getProviderId())
+      .setExternalLogin("old identity")
+      .setExternalIdentityProvider(IDENTITY_PROVIDER.getKey())
+      .setOrganizationUuid(personalOrganization.getUuid()));
+
+    underTest.authenticate(UserIdentityAuthenticatorParameters.builder()
+      .setUserIdentity(USER_IDENTITY)
+      .setProvider(IDENTITY_PROVIDER)
+      .setSource(Source.local(BASIC))
+      .setExistingEmailStrategy(ExistingEmailStrategy.FORBID)
+      .setUpdateLoginStrategy(UpdateLoginStrategy.ALLOW)
+      .build());
+
+    assertThat(db.getDbClient().userDao().selectByUuid(db.getSession(), user.getUuid()))
+      .extracting(UserDto::getLogin, UserDto::getExternalLogin)
+      .contains(USER_LOGIN, USER_IDENTITY.getProviderLogin());
+    OrganizationDto organizationReloaded = db.getDbClient().organizationDao().selectByUuid(db.getSession(), personalOrganization.getUuid()).get();
+    assertThat(organizationReloaded.getKey()).isEqualTo(USER_LOGIN);
+  }
+
+  @Test
+  public void fail_to_authenticate_existing_user_when_personal_org_does_not_exist() {
+    organizationFlags.setEnabled(true);
+    db.users().insertUser(u -> u
+      .setLogin("Old login")
+      .setExternalId(USER_IDENTITY.getProviderId())
+      .setExternalLogin("old identity")
+      .setExternalIdentityProvider(IDENTITY_PROVIDER.getKey())
+      .setOrganizationUuid("unknown"));
+
+    expectedException.expect(IllegalStateException.class);
+    expectedException.expectMessage("Cannot find personal organization uuid 'unknown' for user 'Old login'");
+
+    underTest.authenticate(UserIdentityAuthenticatorParameters.builder()
+      .setUserIdentity(USER_IDENTITY)
+      .setProvider(IDENTITY_PROVIDER)
+      .setSource(Source.local(BASIC))
+      .setExistingEmailStrategy(ExistingEmailStrategy.FORBID)
+      .setUpdateLoginStrategy(UpdateLoginStrategy.ALLOW)
+      .build());
+  }
+
+  @Test
+  public void authenticate_existing_disabled_user() {
+    organizationFlags.setEnabled(true);
+    db.users().insertUser(u -> u
+      .setLogin(USER_LOGIN)
+      .setActive(false)
+      .setName("Old name")
+      .setEmail("Old email")
+      .setExternalId("old id")
+      .setExternalLogin("old identity")
+      .setExternalIdentityProvider("old provide"));
+
+    underTest.authenticate(UserIdentityAuthenticatorParameters.builder()
+      .setUserIdentity(USER_IDENTITY)
+      .setProvider(IDENTITY_PROVIDER)
+      .setSource(Source.local(BASIC))
+      .setExistingEmailStrategy(ExistingEmailStrategy.FORBID)
+      .setUpdateLoginStrategy(UpdateLoginStrategy.ALLOW)
+      .build());
+
+    UserDto userDto = db.users().selectUserByLogin(USER_LOGIN).get();
+    assertThat(userDto.isActive()).isTrue();
+    assertThat(userDto.getName()).isEqualTo("John");
+    assertThat(userDto.getEmail()).isEqualTo("john@email.com");
+    assertThat(userDto.getExternalId()).isEqualTo("ABCD");
+    assertThat(userDto.getExternalLogin()).isEqualTo("johndoo");
+    assertThat(userDto.getExternalIdentityProvider()).isEqualTo("github");
+    assertThat(userDto.isRoot()).isFalse();
+  }
+
+  @Test
+  public void authenticate_existing_user_when_email_already_exists_and_strategy_is_ALLOW() {
+    organizationFlags.setEnabled(true);
+    UserDto existingUser = db.users().insertUser(u -> u.setEmail("john@email.com"));
+    UserDto currentUser = db.users().insertUser(u -> u.setEmail(null));
+    UserIdentity userIdentity = UserIdentity.builder()
+      .setLogin(currentUser.getLogin())
+      .setProviderLogin("johndoo")
+      .setName("John")
+      .setEmail("john@email.com")
+      .build();
+
+    underTest.authenticate(UserIdentityAuthenticatorParameters.builder()
+      .setUserIdentity(userIdentity)
+      .setProvider(IDENTITY_PROVIDER)
+      .setSource(Source.local(BASIC))
+      .setExistingEmailStrategy(ExistingEmailStrategy.ALLOW)
+      .setUpdateLoginStrategy(UpdateLoginStrategy.ALLOW)
+      .build());
+
+    UserDto currentUserReloaded = db.users().selectUserByLogin(currentUser.getLogin()).get();
+    assertThat(currentUserReloaded.getEmail()).isEqualTo("john@email.com");
+    UserDto existingUserReloaded = db.users().selectUserByLogin(existingUser.getLogin()).get();
+    assertThat(existingUserReloaded.getEmail()).isNull();
+  }
+
+  @Test
+  public void throw_EmailAlreadyExistException_when_authenticating_existing_user_when_email_already_exists_and_strategy_is_WARN() {
+    organizationFlags.setEnabled(true);
+    UserDto existingUser = db.users().insertUser(u -> u.setEmail("john@email.com"));
+    UserDto currentUser = db.users().insertUser(u -> u.setEmail(null));
+    UserIdentity userIdentity = UserIdentity.builder()
+      .setLogin(currentUser.getLogin())
+      .setProviderLogin("johndoo")
+      .setName("John")
+      .setEmail("john@email.com")
+      .build();
+
+    expectedException.expect(EmailAlreadyExistsRedirectionException.class);
+
+    underTest.authenticate(UserIdentityAuthenticatorParameters.builder()
+      .setUserIdentity(userIdentity)
+      .setProvider(IDENTITY_PROVIDER)
+      .setSource(Source.local(BASIC))
+      .setExistingEmailStrategy(ExistingEmailStrategy.WARN)
+      .setUpdateLoginStrategy(UpdateLoginStrategy.ALLOW)
+      .build());
+  }
+
+  @Test
+  public void throw_AuthenticationException_when_authenticating_existing_user_when_email_already_exists_and_strategy_is_FORBID() {
+    organizationFlags.setEnabled(true);
+    UserDto existingUser = db.users().insertUser(u -> u.setEmail("john@email.com"));
+    UserDto currentUser = db.users().insertUser(u -> u.setEmail(null));
+    UserIdentity userIdentity = UserIdentity.builder()
+      .setLogin(currentUser.getLogin())
+      .setProviderLogin("johndoo")
+      .setName("John")
+      .setEmail("john@email.com")
+      .build();
+
+    expectedException.expect(authenticationException().from(Source.realm(AuthenticationEvent.Method.FORM, IDENTITY_PROVIDER.getName()))
+      .withLogin(userIdentity.getLogin())
+      .andPublicMessage("You can't sign up because email 'john@email.com' is already used by an existing user. " +
+        "This means that you probably already registered with another account."));
+    expectedException.expectMessage("Email 'john@email.com' is already used");
+
+    underTest.authenticate(UserIdentityAuthenticatorParameters.builder()
+      .setUserIdentity(userIdentity)
+      .setProvider(IDENTITY_PROVIDER)
+      .setSource(Source.realm(AuthenticationEvent.Method.FORM, IDENTITY_PROVIDER.getName()))
+      .setExistingEmailStrategy(ExistingEmailStrategy.FORBID)
+      .setUpdateLoginStrategy(UpdateLoginStrategy.ALLOW)
+      .build());
+  }
+
+  @Test
+  public void does_not_fail_to_authenticate_user_when_email_has_not_changed_and_strategy_is_FORBID() {
+    organizationFlags.setEnabled(true);
+    UserDto currentUser = db.users().insertUser(u -> u.setEmail("john@email.com"));
+    UserIdentity userIdentity = UserIdentity.builder()
+      .setLogin(currentUser.getLogin())
+      .setProviderLogin("johndoo")
+      .setName("John")
+      .setEmail("john@email.com")
+      .build();
+
+    underTest.authenticate(UserIdentityAuthenticatorParameters.builder()
+      .setUserIdentity(userIdentity)
+      .setProvider(IDENTITY_PROVIDER)
+      .setSource(Source.local(BASIC))
+      .setExistingEmailStrategy(ExistingEmailStrategy.FORBID)
+      .setUpdateLoginStrategy(UpdateLoginStrategy.ALLOW)
+      .build());
+
+    UserDto currentUserReloaded = db.users().selectUserByLogin(currentUser.getLogin()).get();
+    assertThat(currentUserReloaded.getEmail()).isEqualTo("john@email.com");
+  }
+
+  @Test
+  public void authenticate_existing_user_and_add_new_groups() {
+    organizationFlags.setEnabled(true);
+    UserDto user = db.users().insertUser(newUserDto()
+      .setLogin(USER_LOGIN)
+      .setActive(true)
+      .setName("John"));
+    GroupDto group1 = db.users().insertGroup(db.getDefaultOrganization(), "group1");
+    GroupDto group2 = db.users().insertGroup(db.getDefaultOrganization(), "group2");
+
+    authenticate(USER_LOGIN, "group1", "group2", "group3");
+
+    checkGroupMembership(user, group1, group2);
+  }
+
+  @Test
+  public void authenticate_existing_user_and_remove_groups() {
+    organizationFlags.setEnabled(true);
+    UserDto user = db.users().insertUser(newUserDto()
+      .setLogin(USER_LOGIN)
+      .setActive(true)
+      .setName("John"));
+    GroupDto group1 = db.users().insertGroup(db.getDefaultOrganization(), "group1");
+    GroupDto group2 = db.users().insertGroup(db.getDefaultOrganization(), "group2");
+    db.users().insertMember(group1, user);
+    db.users().insertMember(group2, user);
+
+    authenticate(USER_LOGIN, "group1");
+
+    checkGroupMembership(user, group1);
+  }
+
+  @Test
+  public void authenticate_existing_user_and_remove_all_groups_expect_default_when_organizations_are_disabled() {
+    organizationFlags.setEnabled(false);
+    UserDto user = db.users().insertUser();
+    GroupDto group1 = db.users().insertGroup(db.getDefaultOrganization(), "group1");
+    GroupDto group2 = db.users().insertGroup(db.getDefaultOrganization(), "group2");
+    GroupDto defaultGroup = insertDefaultGroup();
+    db.users().insertMember(group1, user);
+    db.users().insertMember(group2, user);
+    db.users().insertMember(defaultGroup, user);
+
+    authenticate(user.getLogin());
+
+    checkGroupMembership(user, defaultGroup);
+  }
+
+  @Test
+  public void does_not_force_default_group_when_authenticating_existing_user_when_organizations_are_enabled() {
+    organizationFlags.setEnabled(true);
+    UserDto user = db.users().insertUser();
+    GroupDto group1 = db.users().insertGroup(db.getDefaultOrganization(), "group1");
+    GroupDto defaultGroup = insertDefaultGroup();
+    db.users().insertMember(group1, user);
+    db.users().insertMember(defaultGroup, user);
+
+    authenticate(user.getLogin(), "group1");
+
+    checkGroupMembership(user, group1);
+  }
+
+  @Test
+  public void ignore_groups_on_non_default_organizations() {
+    organizationFlags.setEnabled(true);
+    OrganizationDto org = db.organizations().insert();
+    UserDto user = db.users().insertUser(newUserDto()
+      .setLogin(USER_LOGIN)
+      .setActive(true)
+      .setName("John"));
+    String groupName = "a-group";
+    GroupDto groupInDefaultOrg = db.users().insertGroup(db.getDefaultOrganization(), groupName);
+    GroupDto groupInOrg = db.users().insertGroup(org, groupName);
+
+    // adding a group with the same name than in non-default organization
+    underTest.authenticate(UserIdentityAuthenticatorParameters.builder()
+      .setUserIdentity(UserIdentity.builder()
+        .setProviderLogin("johndoo")
+        .setLogin(user.getLogin())
+        .setName(user.getName())
+        .setGroups(newHashSet(groupName))
+        .build())
+      .setProvider(IDENTITY_PROVIDER)
+      .setSource(Source.local(BASIC))
+      .setExistingEmailStrategy(ExistingEmailStrategy.FORBID)
+      .setUpdateLoginStrategy(UpdateLoginStrategy.ALLOW)
+      .build());
+
+    checkGroupMembership(user, groupInDefaultOrg);
+  }
+
+  private void authenticate(String login, String... groups) {
+    underTest.authenticate(UserIdentityAuthenticatorParameters.builder()
+      .setUserIdentity(UserIdentity.builder()
+        .setProviderLogin("johndoo")
+        .setLogin(login)
+        .setName("John")
+        // No group
+        .setGroups(stream(groups).collect(MoreCollectors.toSet()))
+        .build())
+      .setProvider(IDENTITY_PROVIDER)
+      .setSource(Source.local(BASIC))
+      .setExistingEmailStrategy(ExistingEmailStrategy.FORBID)
+      .setUpdateLoginStrategy(UpdateLoginStrategy.ALLOW)
+      .build());
+  }
+
+  private void checkGroupMembership(UserDto user, GroupDto... expectedGroups) {
+    assertThat(db.users().selectGroupIdsOfUser(user)).containsOnly(stream(expectedGroups).map(GroupDto::getId).collect(Collectors.toList()).toArray(new Integer[] {}));
+  }
+
+  private GroupDto insertDefaultGroup() {
+    return db.users().insertDefaultGroup(db.getDefaultOrganization(), "sonar-users");
+  }
+
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/UserIdentityAuthenticatorTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/UserIdentityAuthenticatorTest.java
deleted file mode 100644 (file)
index 19e6b95..0000000
+++ /dev/null
@@ -1,546 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info 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.authentication;
-
-import java.util.Optional;
-import java.util.stream.Collectors;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.sonar.api.config.internal.MapSettings;
-import org.sonar.api.server.authentication.UserIdentity;
-import org.sonar.api.utils.internal.AlwaysIncreasingSystem2;
-import org.sonar.core.util.stream.MoreCollectors;
-import org.sonar.db.DbTester;
-import org.sonar.db.organization.OrganizationDto;
-import org.sonar.db.user.GroupDto;
-import org.sonar.db.user.UserDto;
-import org.sonar.server.authentication.event.AuthenticationEvent.Method;
-import org.sonar.server.authentication.event.AuthenticationEvent.Source;
-import org.sonar.server.es.EsTester;
-import org.sonar.server.organization.DefaultOrganizationProvider;
-import org.sonar.server.organization.OrganizationCreation;
-import org.sonar.server.organization.TestDefaultOrganizationProvider;
-import org.sonar.server.organization.TestOrganizationFlags;
-import org.sonar.server.user.NewUserNotifier;
-import org.sonar.server.user.UserUpdater;
-import org.sonar.server.user.index.UserIndexer;
-import org.sonar.server.usergroups.DefaultGroupFinder;
-
-import static com.google.common.collect.Sets.newHashSet;
-import static java.util.Arrays.stream;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.mock;
-import static org.sonar.core.config.CorePropertyDefinitions.ONBOARDING_TUTORIAL_SHOW_TO_NEW_USERS;
-import static org.sonar.db.user.UserTesting.newUserDto;
-import static org.sonar.server.authentication.UserIdentityAuthenticator.ExistingEmailStrategy.ALLOW;
-import static org.sonar.server.authentication.UserIdentityAuthenticator.ExistingEmailStrategy.FORBID;
-import static org.sonar.server.authentication.UserIdentityAuthenticator.ExistingEmailStrategy.WARN;
-import static org.sonar.server.authentication.event.AuthenticationExceptionMatcher.authenticationException;
-
-public class UserIdentityAuthenticatorTest {
-
-  private static String USER_LOGIN = "github-johndoo";
-
-  private static UserIdentity USER_IDENTITY = UserIdentity.builder()
-    .setProviderId("ABCD")
-    .setProviderLogin("johndoo")
-    .setLogin(USER_LOGIN)
-    .setName("John")
-    .setEmail("john@email.com")
-    .build();
-
-  private static TestIdentityProvider IDENTITY_PROVIDER = new TestIdentityProvider()
-    .setKey("github")
-    .setName("name of github")
-    .setEnabled(true)
-    .setAllowsUsersToSignUp(true);
-
-  private MapSettings settings = new MapSettings();
-
-  @Rule
-  public ExpectedException expectedException = ExpectedException.none();
-  @Rule
-  public DbTester db = DbTester.create(new AlwaysIncreasingSystem2());
-  @Rule
-  public EsTester es = EsTester.create();
-  private UserIndexer userIndexer = new UserIndexer(db.getDbClient(), es.client());
-  private DefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(db);
-  private OrganizationCreation organizationCreation = mock(OrganizationCreation.class);
-  private TestOrganizationFlags organizationFlags = TestOrganizationFlags.standalone();
-  private LocalAuthentication localAuthentication = new LocalAuthentication(db.getDbClient());
-  private UserUpdater userUpdater = new UserUpdater(
-    mock(NewUserNotifier.class),
-    db.getDbClient(),
-    userIndexer,
-    organizationFlags,
-    defaultOrganizationProvider,
-    organizationCreation,
-    new DefaultGroupFinder(db.getDbClient()),
-    settings.asConfig(),
-    localAuthentication);
-
-  private UserIdentityAuthenticator underTest = new UserIdentityAuthenticator(db.getDbClient(), userUpdater, defaultOrganizationProvider, organizationFlags,
-    new DefaultGroupFinder(db.getDbClient()));
-
-  @Test
-  public void authenticate_new_user() {
-    organizationFlags.setEnabled(true);
-
-    underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, Source.realm(Method.BASIC, IDENTITY_PROVIDER.getName()), FORBID);
-
-    UserDto user = db.users().selectUserByLogin(USER_LOGIN).get();
-    assertThat(user).isNotNull();
-    assertThat(user.isActive()).isTrue();
-    assertThat(user.getName()).isEqualTo("John");
-    assertThat(user.getEmail()).isEqualTo("john@email.com");
-    assertThat(user.getExternalLogin()).isEqualTo("johndoo");
-    assertThat(user.getExternalIdentityProvider()).isEqualTo("github");
-    assertThat(user.getExternalId()).isEqualTo("ABCD");
-    assertThat(user.isRoot()).isFalse();
-    checkGroupMembership(user);
-  }
-
-  @Test
-  public void authenticate_new_user_with_groups() {
-    organizationFlags.setEnabled(true);
-    GroupDto group1 = db.users().insertGroup(db.getDefaultOrganization(), "group1");
-    GroupDto group2 = db.users().insertGroup(db.getDefaultOrganization(), "group2");
-
-    authenticate(USER_LOGIN, "group1", "group2", "group3");
-
-    Optional<UserDto> user = db.users().selectUserByLogin(USER_LOGIN);
-    checkGroupMembership(user.get(), group1, group2);
-  }
-
-  @Test
-  public void authenticate_new_user_and_force_default_group_when_organizations_are_disabled() {
-    organizationFlags.setEnabled(false);
-    UserDto user = db.users().insertUser();
-    GroupDto group1 = db.users().insertGroup(db.getDefaultOrganization(), "group1");
-    GroupDto defaultGroup = insertDefaultGroup();
-    db.users().insertMember(group1, user);
-    db.users().insertMember(defaultGroup, user);
-
-    authenticate(user.getLogin(), "group1");
-
-    checkGroupMembership(user, group1, defaultGroup);
-  }
-
-  @Test
-  public void does_not_force_default_group_when_authenticating_new_user_if_organizations_are_enabled() {
-    organizationFlags.setEnabled(true);
-    UserDto user = db.users().insertUser();
-    GroupDto group1 = db.users().insertGroup(db.getDefaultOrganization(), "group1");
-    GroupDto defaultGroup = insertDefaultGroup();
-    db.users().insertMember(group1, user);
-    db.users().insertMember(defaultGroup, user);
-
-    authenticate(user.getLogin(), "group1");
-
-    checkGroupMembership(user, group1);
-  }
-
-  @Test
-  public void authenticate_new_user_sets_onboarded_flag_to_false_when_onboarding_setting_is_set_to_true() {
-    organizationFlags.setEnabled(true);
-    settings.setProperty(ONBOARDING_TUTORIAL_SHOW_TO_NEW_USERS, true);
-
-    underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, Source.realm(Method.BASIC, IDENTITY_PROVIDER.getName()), FORBID);
-
-    assertThat(db.users().selectUserByLogin(USER_LOGIN).get().isOnboarded()).isFalse();
-  }
-
-  @Test
-  public void authenticate_new_user_sets_onboarded_flag_to_true_when_onboarding_setting_is_set_to_false() {
-    organizationFlags.setEnabled(true);
-    settings.setProperty(ONBOARDING_TUTORIAL_SHOW_TO_NEW_USERS, false);
-
-    underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, Source.realm(Method.BASIC, IDENTITY_PROVIDER.getName()), FORBID);
-
-    assertThat(db.users().selectUserByLogin(USER_LOGIN).get().isOnboarded()).isTrue();
-  }
-
-  @Test
-  public void authenticate_new_user_update_existing_user_email_when_strategy_is_ALLOW() {
-    organizationFlags.setEnabled(true);
-    UserDto existingUser = db.users().insertUser(u -> u.setEmail("john@email.com"));
-    UserIdentity newUser = UserIdentity.builder()
-      .setProviderLogin("johndoo")
-      .setLogin("new_login")
-      .setName(existingUser.getName())
-      .setEmail(existingUser.getEmail())
-      .build();
-
-    underTest.authenticate(newUser, IDENTITY_PROVIDER, Source.local(Method.BASIC), ALLOW);
-
-    UserDto newUserReloaded = db.users().selectUserByLogin(newUser.getLogin()).get();
-    assertThat(newUserReloaded.getEmail()).isEqualTo(existingUser.getEmail());
-    UserDto existingUserReloaded = db.users().selectUserByLogin(existingUser.getLogin()).get();
-    assertThat(existingUserReloaded.getEmail()).isNull();
-  }
-
-  @Test
-  public void external_id_is_set_to_provider_login_when_null() {
-    organizationFlags.setEnabled(true);
-    UserIdentity newUser = UserIdentity.builder()
-      .setProviderId(null)
-      .setLogin("john")
-      .setProviderLogin("johndoo")
-      .setName("JOhn")
-      .build();
-
-    underTest.authenticate(newUser, IDENTITY_PROVIDER, Source.local(Method.BASIC), ALLOW);
-
-    assertThat(db.users().selectUserByLogin(newUser.getLogin()).get())
-      .extracting(UserDto::getLogin, UserDto::getExternalId, UserDto::getExternalLogin)
-      .contains("john", "johndoo", "johndoo");
-  }
-
-  @Test
-  public void throw_EmailAlreadyExistException_when_authenticating_new_user_when_email_already_exists_and_strategy_is_WARN() {
-    organizationFlags.setEnabled(true);
-    UserDto existingUser = db.users().insertUser(u -> u.setEmail("john@email.com"));
-    UserIdentity newUser = UserIdentity.builder()
-      .setProviderLogin("johndoo")
-      .setLogin("new_login")
-      .setName(existingUser.getName())
-      .setEmail(existingUser.getEmail())
-      .build();
-
-    expectedException.expect(EmailAlreadyExistsException.class);
-
-    underTest.authenticate(newUser, IDENTITY_PROVIDER, Source.local(Method.BASIC), WARN);
-  }
-
-  @Test
-  public void throw_AuthenticationException_when_authenticating_new_user_when_email_already_exists_and_strategy_is_FORBID() {
-    db.users().insertUser(newUserDto()
-      .setLogin("Existing user with same email")
-      .setActive(true)
-      .setEmail("john@email.com"));
-    Source source = Source.realm(Method.FORM, IDENTITY_PROVIDER.getName());
-
-    expectedException.expect(authenticationException().from(source)
-      .withLogin(USER_IDENTITY.getLogin())
-      .andPublicMessage("You can't sign up because email 'john@email.com' is already used by an existing user. " +
-        "This means that you probably already registered with another account."));
-    expectedException.expectMessage("Email 'john@email.com' is already used");
-
-    underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, source, FORBID);
-  }
-
-  @Test
-  public void fail_to_authenticate_new_user_when_allow_users_to_signup_is_false() {
-    TestIdentityProvider identityProvider = new TestIdentityProvider()
-      .setKey("github")
-      .setName("Github")
-      .setEnabled(true)
-      .setAllowsUsersToSignUp(false);
-    Source source = Source.realm(Method.FORM, identityProvider.getName());
-
-    expectedException.expect(authenticationException().from(source).withLogin(USER_IDENTITY.getLogin()).andPublicMessage("'github' users are not allowed to sign up"));
-    expectedException.expectMessage("User signup disabled for provider 'github'");
-    underTest.authenticate(USER_IDENTITY, identityProvider, source, FORBID);
-  }
-
-  @Test
-  public void authenticate_existing_user_matching_login() {
-    db.users().insertUser(u -> u
-      .setLogin(USER_LOGIN)
-      .setName("Old name")
-      .setEmail("Old email")
-      .setExternalId("old id")
-      .setExternalLogin("old identity")
-      .setExternalIdentityProvider("old provide"));
-
-    underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, Source.local(Method.BASIC), FORBID);
-
-    assertThat(db.users().selectUserByLogin(USER_LOGIN).get())
-      .extracting(UserDto::getName, UserDto::getEmail, UserDto::getExternalId, UserDto::getExternalLogin, UserDto::getExternalIdentityProvider, UserDto::isActive)
-      .contains("John", "john@email.com", "ABCD", "johndoo", "github", true);
-  }
-
-  @Test
-  public void authenticate_existing_user_matching_external_id() {
-    UserDto user = db.users().insertUser(u -> u
-      .setLogin("Old login")
-      .setName("Old name")
-      .setEmail("Old email")
-      .setExternalId(USER_IDENTITY.getProviderId())
-      .setExternalLogin("old identity")
-      .setExternalIdentityProvider(IDENTITY_PROVIDER.getKey()));
-
-    underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, Source.local(Method.BASIC), FORBID);
-
-    assertThat(db.users().selectUserByLogin("Old login")).isNotPresent();
-    assertThat(db.getDbClient().userDao().selectByUuid(db.getSession(), user.getUuid()))
-      .extracting(UserDto::getLogin, UserDto::getName, UserDto::getEmail, UserDto::getExternalId, UserDto::getExternalLogin, UserDto::getExternalIdentityProvider,
-        UserDto::isActive)
-      .contains(USER_LOGIN, "John", "john@email.com", "ABCD", "johndoo", "github", true);
-  }
-
-  @Test
-  public void authenticate_existing_user_and_update_only_login() {
-    UserDto user = db.users().insertUser(u -> u
-      .setLogin("old login")
-      .setName(USER_IDENTITY.getName())
-      .setEmail(USER_IDENTITY.getEmail())
-      .setExternalId(USER_IDENTITY.getProviderId())
-      .setExternalLogin("old identity")
-      .setExternalIdentityProvider(IDENTITY_PROVIDER.getKey()));
-
-    underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, Source.local(Method.BASIC), FORBID);
-
-    assertThat(db.users().selectUserByLogin("Old login")).isNotPresent();
-    assertThat(db.getDbClient().userDao().selectByUuid(db.getSession(), user.getUuid()))
-      .extracting(UserDto::getLogin, UserDto::getName, UserDto::getEmail, UserDto::getExternalId, UserDto::getExternalLogin, UserDto::getExternalIdentityProvider,
-        UserDto::isActive)
-      .containsExactlyInAnyOrder(USER_LOGIN, USER_IDENTITY.getName(), USER_IDENTITY.getEmail(), USER_IDENTITY.getProviderId(), USER_IDENTITY.getProviderLogin(), IDENTITY_PROVIDER.getKey(),
-        true);
-  }
-
-  @Test
-  public void authenticate_existing_user_matching_login_when_external_id_is_null() {
-    UserDto user = db.users().insertUser(u -> u
-      .setLogin(USER_LOGIN)
-      .setName("Old name")
-      .setEmail("Old email")
-      .setExternalId("Old id")
-      .setExternalLogin("old identity")
-      .setExternalIdentityProvider(IDENTITY_PROVIDER.getKey()));
-
-    underTest.authenticate(UserIdentity.builder()
-      .setProviderId(null)
-      .setProviderLogin("johndoo")
-      .setLogin(USER_LOGIN)
-      .setName("John")
-      .setEmail("john@email.com")
-      .build(),
-      IDENTITY_PROVIDER, Source.local(Method.BASIC), FORBID);
-
-    assertThat(db.getDbClient().userDao().selectByUuid(db.getSession(), user.getUuid()))
-      .extracting(UserDto::getLogin, UserDto::getName, UserDto::getEmail, UserDto::getExternalId, UserDto::getExternalLogin, UserDto::getExternalIdentityProvider,
-        UserDto::isActive)
-      .contains(user.getLogin(), "John", "john@email.com", "johndoo", "johndoo", "github", true);
-  }
-
-  @Test
-  public void authenticate_existing_disabled_user() {
-    organizationFlags.setEnabled(true);
-    db.users().insertUser(u -> u
-      .setLogin(USER_LOGIN)
-      .setActive(false)
-      .setName("Old name")
-      .setEmail("Old email")
-      .setExternalId("old id")
-      .setExternalLogin("old identity")
-      .setExternalIdentityProvider("old provide"));
-
-    underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, Source.local(Method.BASIC_TOKEN), FORBID);
-
-    UserDto userDto = db.users().selectUserByLogin(USER_LOGIN).get();
-    assertThat(userDto.isActive()).isTrue();
-    assertThat(userDto.getName()).isEqualTo("John");
-    assertThat(userDto.getEmail()).isEqualTo("john@email.com");
-    assertThat(userDto.getExternalId()).isEqualTo("ABCD");
-    assertThat(userDto.getExternalLogin()).isEqualTo("johndoo");
-    assertThat(userDto.getExternalIdentityProvider()).isEqualTo("github");
-    assertThat(userDto.isRoot()).isFalse();
-  }
-
-  @Test
-  public void authenticate_existing_user_when_email_already_exists_and_strategy_is_ALLOW() {
-    organizationFlags.setEnabled(true);
-    UserDto existingUser = db.users().insertUser(u -> u.setEmail("john@email.com"));
-    UserDto currentUser = db.users().insertUser(u -> u.setEmail(null));
-    UserIdentity userIdentity = UserIdentity.builder()
-      .setLogin(currentUser.getLogin())
-      .setProviderLogin("johndoo")
-      .setName("John")
-      .setEmail("john@email.com")
-      .build();
-
-    underTest.authenticate(userIdentity, IDENTITY_PROVIDER, Source.local(Method.BASIC), ALLOW);
-
-    UserDto currentUserReloaded = db.users().selectUserByLogin(currentUser.getLogin()).get();
-    assertThat(currentUserReloaded.getEmail()).isEqualTo("john@email.com");
-    UserDto existingUserReloaded = db.users().selectUserByLogin(existingUser.getLogin()).get();
-    assertThat(existingUserReloaded.getEmail()).isNull();
-  }
-
-  @Test
-  public void throw_EmailAlreadyExistException_when_authenticating_existing_user_when_email_already_exists_and_strategy_is_WARN() {
-    organizationFlags.setEnabled(true);
-    UserDto existingUser = db.users().insertUser(u -> u.setEmail("john@email.com"));
-    UserDto currentUser = db.users().insertUser(u -> u.setEmail(null));
-    UserIdentity userIdentity = UserIdentity.builder()
-      .setLogin(currentUser.getLogin())
-      .setProviderLogin("johndoo")
-      .setName("John")
-      .setEmail("john@email.com")
-      .build();
-
-    expectedException.expect(EmailAlreadyExistsException.class);
-
-    underTest.authenticate(userIdentity, IDENTITY_PROVIDER, Source.local(Method.BASIC), WARN);
-  }
-
-  @Test
-  public void throw_AuthenticationException_when_authenticating_existing_user_when_email_already_exists_and_strategy_is_FORBID() {
-    organizationFlags.setEnabled(true);
-    UserDto existingUser = db.users().insertUser(u -> u.setEmail("john@email.com"));
-    UserDto currentUser = db.users().insertUser(u -> u.setEmail(null));
-    UserIdentity userIdentity = UserIdentity.builder()
-      .setLogin(currentUser.getLogin())
-      .setProviderLogin("johndoo")
-      .setName("John")
-      .setEmail("john@email.com")
-      .build();
-
-    expectedException.expect(authenticationException().from(Source.realm(Method.FORM, IDENTITY_PROVIDER.getName()))
-      .withLogin(userIdentity.getLogin())
-      .andPublicMessage("You can't sign up because email 'john@email.com' is already used by an existing user. " +
-        "This means that you probably already registered with another account."));
-    expectedException.expectMessage("Email 'john@email.com' is already used");
-
-    underTest.authenticate(userIdentity, IDENTITY_PROVIDER, Source.realm(Method.FORM, IDENTITY_PROVIDER.getName()), FORBID);
-  }
-
-  @Test
-  public void does_not_fail_to_authenticate_user_when_email_has_not_changed_and_strategy_is_FORBID() {
-    organizationFlags.setEnabled(true);
-    UserDto currentUser = db.users().insertUser(u -> u.setEmail("john@email.com"));
-    UserIdentity userIdentity = UserIdentity.builder()
-      .setLogin(currentUser.getLogin())
-      .setProviderLogin("johndoo")
-      .setName("John")
-      .setEmail("john@email.com")
-      .build();
-
-    underTest.authenticate(userIdentity, IDENTITY_PROVIDER, Source.local(Method.BASIC), FORBID);
-
-    UserDto currentUserReloaded = db.users().selectUserByLogin(currentUser.getLogin()).get();
-    assertThat(currentUserReloaded.getEmail()).isEqualTo("john@email.com");
-  }
-
-  @Test
-  public void authenticate_existing_user_and_add_new_groups() {
-    organizationFlags.setEnabled(true);
-    UserDto user = db.users().insertUser(newUserDto()
-      .setLogin(USER_LOGIN)
-      .setActive(true)
-      .setName("John"));
-    GroupDto group1 = db.users().insertGroup(db.getDefaultOrganization(), "group1");
-    GroupDto group2 = db.users().insertGroup(db.getDefaultOrganization(), "group2");
-
-    authenticate(USER_LOGIN, "group1", "group2", "group3");
-
-    checkGroupMembership(user, group1, group2);
-  }
-
-  @Test
-  public void authenticate_existing_user_and_remove_groups() {
-    organizationFlags.setEnabled(true);
-    UserDto user = db.users().insertUser(newUserDto()
-      .setLogin(USER_LOGIN)
-      .setActive(true)
-      .setName("John"));
-    GroupDto group1 = db.users().insertGroup(db.getDefaultOrganization(), "group1");
-    GroupDto group2 = db.users().insertGroup(db.getDefaultOrganization(), "group2");
-    db.users().insertMember(group1, user);
-    db.users().insertMember(group2, user);
-
-    authenticate(USER_LOGIN, "group1");
-
-    checkGroupMembership(user, group1);
-  }
-
-  @Test
-  public void authenticate_existing_user_and_remove_all_groups_expect_default_when_organizations_are_disabled() {
-    organizationFlags.setEnabled(false);
-    UserDto user = db.users().insertUser();
-    GroupDto group1 = db.users().insertGroup(db.getDefaultOrganization(), "group1");
-    GroupDto group2 = db.users().insertGroup(db.getDefaultOrganization(), "group2");
-    GroupDto defaultGroup = insertDefaultGroup();
-    db.users().insertMember(group1, user);
-    db.users().insertMember(group2, user);
-    db.users().insertMember(defaultGroup, user);
-
-    authenticate(user.getLogin());
-
-    checkGroupMembership(user, defaultGroup);
-  }
-
-  @Test
-  public void does_not_force_default_group_when_authenticating_existing_user_when_organizations_are_enabled() {
-    organizationFlags.setEnabled(true);
-    UserDto user = db.users().insertUser();
-    GroupDto group1 = db.users().insertGroup(db.getDefaultOrganization(), "group1");
-    GroupDto defaultGroup = insertDefaultGroup();
-    db.users().insertMember(group1, user);
-    db.users().insertMember(defaultGroup, user);
-
-    authenticate(user.getLogin(), "group1");
-
-    checkGroupMembership(user, group1);
-  }
-
-  @Test
-  public void ignore_groups_on_non_default_organizations() {
-    organizationFlags.setEnabled(true);
-    OrganizationDto org = db.organizations().insert();
-    UserDto user = db.users().insertUser(newUserDto()
-      .setLogin(USER_LOGIN)
-      .setActive(true)
-      .setName("John"));
-    String groupName = "a-group";
-    GroupDto groupInDefaultOrg = db.users().insertGroup(db.getDefaultOrganization(), groupName);
-    GroupDto groupInOrg = db.users().insertGroup(org, groupName);
-
-    // adding a group with the same name than in non-default organization
-    underTest.authenticate(UserIdentity.builder()
-      .setProviderLogin("johndoo")
-      .setLogin(user.getLogin())
-      .setName(user.getName())
-      .setGroups(newHashSet(groupName))
-      .build(), IDENTITY_PROVIDER, Source.sso(), FORBID);
-
-    checkGroupMembership(user, groupInDefaultOrg);
-  }
-
-  private void authenticate(String login, String... groups) {
-    underTest.authenticate(UserIdentity.builder()
-      .setProviderLogin("johndoo")
-      .setLogin(login)
-      .setName("John")
-      // No group
-      .setGroups(stream(groups).collect(MoreCollectors.toSet()))
-      .build(), IDENTITY_PROVIDER, Source.sso(), FORBID);
-  }
-
-  private void checkGroupMembership(UserDto user, GroupDto... expectedGroups) {
-    assertThat(db.users().selectGroupIdsOfUser(user)).containsOnly(stream(expectedGroups).map(GroupDto::getId).collect(Collectors.toList()).toArray(new Integer[] {}));
-  }
-
-  private GroupDto insertDefaultGroup() {
-    return db.users().insertDefaultGroup(db.getDefaultOrganization(), "sonar-users");
-  }
-}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/organization/OrganizationCreationImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/organization/OrganizationCreationImplTest.java
deleted file mode 100644 (file)
index b1a5a46..0000000
+++ /dev/null
@@ -1,595 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info 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.organization;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.Optional;
-import org.apache.commons.lang.RandomStringUtils;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.sonar.api.config.internal.MapSettings;
-import org.sonar.api.utils.System2;
-import org.sonar.api.utils.internal.TestSystem2;
-import org.sonar.api.web.UserRole;
-import org.sonar.core.config.CorePropertyDefinitions;
-import org.sonar.core.permission.GlobalPermissions;
-import org.sonar.core.util.SequenceUuidFactory;
-import org.sonar.core.util.UuidFactory;
-import org.sonar.db.DbClient;
-import org.sonar.db.DbSession;
-import org.sonar.db.DbTester;
-import org.sonar.db.organization.DefaultTemplates;
-import org.sonar.db.organization.OrganizationDto;
-import org.sonar.db.permission.template.PermissionTemplateCharacteristicDto;
-import org.sonar.db.permission.template.PermissionTemplateDto;
-import org.sonar.db.permission.template.PermissionTemplateGroupDto;
-import org.sonar.db.qualitygate.QualityGateDto;
-import org.sonar.db.qualityprofile.QProfileDto;
-import org.sonar.db.qualityprofile.RulesProfileDto;
-import org.sonar.db.user.GroupDto;
-import org.sonar.db.user.UserDto;
-import org.sonar.db.user.UserMembershipDto;
-import org.sonar.db.user.UserMembershipQuery;
-import org.sonar.server.es.EsTester;
-import org.sonar.server.es.SearchOptions;
-import org.sonar.server.qualitygate.QualityGateFinder;
-import org.sonar.server.qualityprofile.BuiltInQProfile;
-import org.sonar.server.qualityprofile.BuiltInQProfileRepositoryRule;
-import org.sonar.server.qualityprofile.QProfileName;
-import org.sonar.server.user.index.UserIndex;
-import org.sonar.server.user.index.UserIndexer;
-import org.sonar.server.user.index.UserQuery;
-import org.sonar.server.usergroups.DefaultGroupCreator;
-import org.sonar.server.usergroups.DefaultGroupCreatorImpl;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.fail;
-import static org.assertj.core.api.Assertions.tuple;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-import static org.sonar.server.language.LanguageTesting.newLanguage;
-import static org.sonar.server.organization.OrganizationCreation.NewOrganization.newOrganizationBuilder;
-
-public class OrganizationCreationImplTest {
-  private static final long A_DATE = 12893434L;
-  private static final String A_LOGIN = "a-login";
-  private static final String SLUG_OF_A_LOGIN = "slug-of-a-login";
-  private static final String STRING_64_CHARS = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
-  private static final String A_NAME = "a name";
-
-  private OrganizationCreation.NewOrganization FULL_POPULATED_NEW_ORGANIZATION = newOrganizationBuilder()
-    .setName("a-name")
-    .setKey("a-key")
-    .setDescription("a-description")
-    .setUrl("a-url")
-    .setAvatarUrl("a-avatar")
-    .build();
-
-  private System2 system2 = new TestSystem2().setNow(A_DATE);
-
-  @Rule
-  public DbTester db = DbTester.create(system2);
-  @Rule
-  public EsTester es = EsTester.create();
-  @Rule
-  public ExpectedException expectedException = ExpectedException.none();
-  @Rule
-  public BuiltInQProfileRepositoryRule builtInQProfileRepositoryRule = new BuiltInQProfileRepositoryRule();
-
-  private DbSession dbSession = db.getSession();
-
-  private IllegalArgumentException exceptionThrownByOrganizationValidation = new IllegalArgumentException("simulate IAE thrown by OrganizationValidation");
-  private DbClient dbClient = db.getDbClient();
-  private UuidFactory uuidFactory = new SequenceUuidFactory();
-  private OrganizationValidation organizationValidation = mock(OrganizationValidation.class);
-  private MapSettings settings = new MapSettings();
-  private UserIndexer userIndexer = new UserIndexer(dbClient, es.client());
-  private UserIndex userIndex = new UserIndex(es.client(), system2);
-  private DefaultGroupCreator defaultGroupCreator = new DefaultGroupCreatorImpl(dbClient);
-  private QualityGateFinder qualityGateFinder = new QualityGateFinder(dbClient);
-  private OrganizationCreationImpl underTest = new OrganizationCreationImpl(dbClient, system2, uuidFactory, organizationValidation, settings.asConfig(), userIndexer,
-    builtInQProfileRepositoryRule, defaultGroupCreator);
-
-  @Test
-  public void create_throws_NPE_if_NewOrganization_arg_is_null() throws OrganizationCreation.KeyConflictException {
-    UserDto user = db.users().insertUser();
-
-    expectedException.expect(NullPointerException.class);
-    expectedException.expectMessage("newOrganization can't be null");
-
-    underTest.create(dbSession, user, null);
-  }
-
-  @Test
-  public void create_throws_exception_thrown_by_checkValidKey() throws OrganizationCreation.KeyConflictException {
-    UserDto user = db.users().insertUser();
-
-    when(organizationValidation.checkKey(FULL_POPULATED_NEW_ORGANIZATION.getKey()))
-      .thenThrow(exceptionThrownByOrganizationValidation);
-
-    createThrowsExceptionThrownByOrganizationValidation(user);
-  }
-
-  @Test
-  public void create_throws_exception_thrown_by_checkValidDescription() throws OrganizationCreation.KeyConflictException {
-    UserDto user = db.users().insertUser();
-
-    when(organizationValidation.checkDescription(FULL_POPULATED_NEW_ORGANIZATION.getDescription())).thenThrow(exceptionThrownByOrganizationValidation);
-
-    createThrowsExceptionThrownByOrganizationValidation(user);
-  }
-
-  @Test
-  public void create_throws_exception_thrown_by_checkValidUrl() throws OrganizationCreation.KeyConflictException {
-    UserDto user = db.users().insertUser();
-
-    when(organizationValidation.checkUrl(FULL_POPULATED_NEW_ORGANIZATION.getUrl())).thenThrow(exceptionThrownByOrganizationValidation);
-
-    createThrowsExceptionThrownByOrganizationValidation(user);
-  }
-
-  @Test
-  public void create_throws_exception_thrown_by_checkValidAvatar() throws OrganizationCreation.KeyConflictException {
-    UserDto user = db.users().insertUser();
-
-    when(organizationValidation.checkAvatar(FULL_POPULATED_NEW_ORGANIZATION.getAvatar())).thenThrow(exceptionThrownByOrganizationValidation);
-
-    createThrowsExceptionThrownByOrganizationValidation(user);
-  }
-
-  private void createThrowsExceptionThrownByOrganizationValidation(UserDto user) throws OrganizationCreation.KeyConflictException {
-    try {
-      underTest.create(dbSession, user, FULL_POPULATED_NEW_ORGANIZATION);
-      fail(exceptionThrownByOrganizationValidation + " should have been thrown");
-    } catch (IllegalArgumentException e) {
-      assertThat(e).isSameAs(exceptionThrownByOrganizationValidation);
-    }
-  }
-
-  @Test
-  public void create_fails_with_ISE_if_BuiltInQProfileRepository_has_not_been_initialized() throws OrganizationCreation.KeyConflictException {
-    UserDto user = db.users().insertUser();
-    db.qualityGates().insertBuiltInQualityGate();
-
-    expectedException.expect(IllegalStateException.class);
-    expectedException.expectMessage("initialize must be called first");
-
-    underTest.create(dbSession, user, FULL_POPULATED_NEW_ORGANIZATION);
-  }
-
-  @Test
-  public void create_fails_with_KeyConflictException_if_org_with_key_in_NewOrganization_arg_already_exists_in_db() throws OrganizationCreation.KeyConflictException {
-    db.organizations().insertForKey(FULL_POPULATED_NEW_ORGANIZATION.getKey());
-    UserDto user = db.users().insertUser();
-
-    expectedException.expect(OrganizationCreation.KeyConflictException.class);
-    expectedException.expectMessage("Organization key '" + FULL_POPULATED_NEW_ORGANIZATION.getKey() + "' is already used");
-
-    underTest.create(dbSession, user, FULL_POPULATED_NEW_ORGANIZATION);
-  }
-
-  @Test
-  public void create_creates_unguarded_organization_with_properties_from_NewOrganization_arg() throws OrganizationCreation.KeyConflictException {
-    builtInQProfileRepositoryRule.initialize();
-    UserDto user = db.users().insertUser();
-    db.qualityGates().insertBuiltInQualityGate();
-
-    underTest.create(dbSession, user, FULL_POPULATED_NEW_ORGANIZATION);
-
-    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, FULL_POPULATED_NEW_ORGANIZATION.getKey()).get();
-    assertThat(organization.getUuid()).isNotEmpty();
-    assertThat(organization.getKey()).isEqualTo(FULL_POPULATED_NEW_ORGANIZATION.getKey());
-    assertThat(organization.getName()).isEqualTo(FULL_POPULATED_NEW_ORGANIZATION.getName());
-    assertThat(organization.getDescription()).isEqualTo(FULL_POPULATED_NEW_ORGANIZATION.getDescription());
-    assertThat(organization.getUrl()).isEqualTo(FULL_POPULATED_NEW_ORGANIZATION.getUrl());
-    assertThat(organization.getAvatarUrl()).isEqualTo(FULL_POPULATED_NEW_ORGANIZATION.getAvatar());
-    assertThat(organization.isGuarded()).isFalse();
-    assertThat(organization.getUserId()).isNull();
-    assertThat(organization.getCreatedAt()).isEqualTo(A_DATE);
-    assertThat(organization.getUpdatedAt()).isEqualTo(A_DATE);
-  }
-
-  @Test
-  public void create_creates_owners_group_with_all_permissions_for_new_organization_and_add_current_user_to_it() throws OrganizationCreation.KeyConflictException {
-    UserDto user = db.users().insertUser();
-    builtInQProfileRepositoryRule.initialize();
-    db.qualityGates().insertBuiltInQualityGate();
-
-    underTest.create(dbSession, user, FULL_POPULATED_NEW_ORGANIZATION);
-
-    verifyGroupOwners(user, FULL_POPULATED_NEW_ORGANIZATION.getKey(), FULL_POPULATED_NEW_ORGANIZATION.getName());
-  }
-
-  @Test
-  public void create_creates_members_group_and_add_current_user_to_it() throws OrganizationCreation.KeyConflictException {
-    UserDto user = db.users().insertUser();
-    builtInQProfileRepositoryRule.initialize();
-    db.qualityGates().insertBuiltInQualityGate();
-
-    underTest.create(dbSession, user, FULL_POPULATED_NEW_ORGANIZATION);
-
-    verifyMembersGroup(user, FULL_POPULATED_NEW_ORGANIZATION.getKey());
-  }
-
-  @Test
-  public void create_does_not_require_description_url_and_avatar_to_be_non_null() throws OrganizationCreation.KeyConflictException {
-    builtInQProfileRepositoryRule.initialize();
-    UserDto user = db.users().insertUser();
-    db.qualityGates().insertBuiltInQualityGate();
-
-    underTest.create(dbSession, user, newOrganizationBuilder()
-      .setKey("key")
-      .setName("name")
-      .build());
-
-    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, "key").get();
-    assertThat(organization.getKey()).isEqualTo("key");
-    assertThat(organization.getName()).isEqualTo("name");
-    assertThat(organization.getDescription()).isNull();
-    assertThat(organization.getUrl()).isNull();
-    assertThat(organization.getAvatarUrl()).isNull();
-    assertThat(organization.isGuarded()).isFalse();
-    assertThat(organization.getUserId()).isNull();
-  }
-
-  @Test
-  public void create_creates_default_template_for_new_organization() throws OrganizationCreation.KeyConflictException {
-    builtInQProfileRepositoryRule.initialize();
-    UserDto user = db.users().insertUser();
-    db.qualityGates().insertBuiltInQualityGate();
-
-    underTest.create(dbSession, user, FULL_POPULATED_NEW_ORGANIZATION);
-
-    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, FULL_POPULATED_NEW_ORGANIZATION.getKey()).get();
-    GroupDto ownersGroup = dbClient.groupDao().selectByName(dbSession, organization.getUuid(), "Owners").get();
-    int defaultGroupId = dbClient.organizationDao().getDefaultGroupId(dbSession, organization.getUuid()).get();
-    PermissionTemplateDto defaultTemplate = dbClient.permissionTemplateDao().selectByName(dbSession, organization.getUuid(), "default template");
-    assertThat(defaultTemplate.getName()).isEqualTo("Default template");
-    assertThat(defaultTemplate.getDescription()).isEqualTo("Default permission template of organization " + FULL_POPULATED_NEW_ORGANIZATION.getName());
-    DefaultTemplates defaultTemplates = dbClient.organizationDao().getDefaultTemplates(dbSession, organization.getUuid()).get();
-    assertThat(defaultTemplates.getProjectUuid()).isEqualTo(defaultTemplate.getUuid());
-    assertThat(defaultTemplates.getViewUuid()).isNull();
-    assertThat(dbClient.permissionTemplateDao().selectGroupPermissionsByTemplateId(dbSession, defaultTemplate.getId()))
-      .extracting(PermissionTemplateGroupDto::getGroupId, PermissionTemplateGroupDto::getPermission)
-      .containsOnly(
-        tuple(ownersGroup.getId(), UserRole.ADMIN), tuple(ownersGroup.getId(), UserRole.ISSUE_ADMIN), tuple(ownersGroup.getId(), GlobalPermissions.SCAN_EXECUTION),
-        tuple(defaultGroupId, UserRole.USER), tuple(defaultGroupId, UserRole.CODEVIEWER));
-  }
-
-  @Test
-  public void create_add_current_user_as_member_of_organization() throws OrganizationCreation.KeyConflictException {
-    UserDto user = db.users().insertUser();
-    builtInQProfileRepositoryRule.initialize();
-    db.qualityGates().insertBuiltInQualityGate();
-
-    OrganizationDto result = underTest.create(dbSession, user, FULL_POPULATED_NEW_ORGANIZATION);
-
-    assertThat(dbClient.organizationMemberDao().select(dbSession, result.getUuid(), user.getId())).isPresent();
-    assertThat(userIndex.search(UserQuery.builder().setOrganizationUuid(result.getUuid()).setTextQuery(user.getLogin()).build(), new SearchOptions()).getTotal()).isEqualTo(1L);
-  }
-
-  @Test
-  public void create_associates_to_built_in_quality_profiles() throws OrganizationCreation.KeyConflictException {
-    BuiltInQProfile builtIn1 = builtInQProfileRepositoryRule.add(newLanguage("foo"), "qp1", true);
-    BuiltInQProfile builtIn2 = builtInQProfileRepositoryRule.add(newLanguage("foo"), "qp2");
-    builtInQProfileRepositoryRule.initialize();
-    insertRulesProfile(builtIn1);
-    insertRulesProfile(builtIn2);
-    UserDto user = db.users().insertUser();
-    db.qualityGates().insertBuiltInQualityGate();
-
-    underTest.create(dbSession, user, FULL_POPULATED_NEW_ORGANIZATION);
-
-    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, FULL_POPULATED_NEW_ORGANIZATION.getKey()).get();
-    List<QProfileDto> profiles = dbClient.qualityProfileDao().selectOrderedByOrganizationUuid(dbSession, organization);
-    assertThat(profiles).extracting(p -> new QProfileName(p.getLanguage(), p.getName())).containsExactlyInAnyOrder(
-      builtIn1.getQProfileName(), builtIn2.getQProfileName());
-    assertThat(dbClient.qualityProfileDao().selectDefaultProfile(dbSession, organization, "foo").getName())
-      .isEqualTo("qp1");
-  }
-
-  private void insertRulesProfile(BuiltInQProfile builtIn) {
-    RulesProfileDto dto = new RulesProfileDto()
-      .setIsBuiltIn(true)
-      .setKee(RandomStringUtils.randomAlphabetic(40))
-      .setLanguage(builtIn.getLanguage())
-      .setName(builtIn.getName());
-    dbClient.qualityProfileDao().insert(db.getSession(), dto);
-    db.commit();
-  }
-
-  @Test
-  public void create_associates_to_built_in_quality_gate() throws OrganizationCreation.KeyConflictException {
-    QualityGateDto builtInQualityGate = db.qualityGates().insertBuiltInQualityGate();
-    builtInQProfileRepositoryRule.initialize();
-    UserDto user = db.users().insertUser();
-
-    underTest.create(dbSession, user, FULL_POPULATED_NEW_ORGANIZATION);
-
-    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, FULL_POPULATED_NEW_ORGANIZATION.getKey()).get();
-    assertThat(dbClient.qualityGateDao().selectDefault(dbSession, organization).getUuid()).isEqualTo(builtInQualityGate.getUuid());
-  }
-
-  @Test
-  public void createForUser_has_no_effect_if_setting_for_feature_is_not_set() {
-    checkSizeOfTables();
-
-    underTest.createForUser(null /* argument is not even read */, null /* argument is not even read */);
-
-    checkSizeOfTables();
-  }
-
-  @Test
-  public void createForUser_has_no_effect_if_setting_for_feature_is_disabled() {
-    enableCreatePersonalOrg(false);
-
-    checkSizeOfTables();
-
-    underTest.createForUser(null /* argument is not even read */, null /* argument is not even read */);
-
-    checkSizeOfTables();
-  }
-
-  private void checkSizeOfTables() {
-    assertThat(db.countRowsOfTable("organizations")).isEqualTo(1);
-    assertThat(db.countRowsOfTable("groups")).isEqualTo(0);
-    assertThat(db.countRowsOfTable("groups_users")).isEqualTo(0);
-    assertThat(db.countRowsOfTable("permission_templates")).isEqualTo(0);
-    assertThat(db.countRowsOfTable("perm_templates_users")).isEqualTo(0);
-    assertThat(db.countRowsOfTable("perm_templates_groups")).isEqualTo(0);
-  }
-
-  @Test
-  public void createForUser_creates_guarded_organization_with_key_name_and_description_generated_from_user_login_and_name_and_associated_to_user() {
-    UserDto user = db.users().insertUser(A_LOGIN);
-    when(organizationValidation.generateKeyFrom(A_LOGIN)).thenReturn(SLUG_OF_A_LOGIN);
-    enableCreatePersonalOrg(true);
-    builtInQProfileRepositoryRule.initialize();
-    db.qualityGates().insertBuiltInQualityGate();
-
-    underTest.createForUser(dbSession, user);
-
-    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, SLUG_OF_A_LOGIN).get();
-    assertThat(organization.getUuid()).isNotEmpty();
-    assertThat(organization.getKey()).isEqualTo(SLUG_OF_A_LOGIN);
-    assertThat(organization.getName()).isEqualTo(user.getName());
-    assertThat(organization.getDescription()).isEqualTo(user.getName() + "'s personal organization");
-    assertThat(organization.getUrl()).isNull();
-    assertThat(organization.getAvatarUrl()).isNull();
-    assertThat(organization.isGuarded()).isTrue();
-    assertThat(organization.getUserId()).isEqualTo(user.getId());
-    assertThat(organization.getCreatedAt()).isEqualTo(A_DATE);
-    assertThat(organization.getUpdatedAt()).isEqualTo(A_DATE);
-  }
-
-  @Test
-  public void createForUser_fails_with_ISE_if_organization_with_slug_of_login_already_exists() {
-    UserDto user = db.users().insertUser(A_LOGIN);
-    when(organizationValidation.generateKeyFrom(A_LOGIN)).thenReturn(SLUG_OF_A_LOGIN);
-    db.organizations().insertForKey(SLUG_OF_A_LOGIN);
-    enableCreatePersonalOrg(true);
-
-    expectedException.expect(IllegalStateException.class);
-    expectedException.expectMessage("Can't create organization with key '" + SLUG_OF_A_LOGIN + "' for new user '" + A_LOGIN
-      + "' because an organization with this key already exists");
-
-    underTest.createForUser(dbSession, user);
-  }
-
-  @Test
-  public void createForUser_gives_all_permissions_for_new_organization_to_current_user() {
-    UserDto user = db.users().insertUser(dto -> dto.setLogin(A_LOGIN).setName(A_NAME));
-    when(organizationValidation.generateKeyFrom(A_LOGIN)).thenReturn(SLUG_OF_A_LOGIN);
-    enableCreatePersonalOrg(true);
-    builtInQProfileRepositoryRule.initialize();
-    db.qualityGates().insertBuiltInQualityGate();
-
-    underTest.createForUser(dbSession, user);
-
-    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, SLUG_OF_A_LOGIN).get();
-    assertThat(dbClient.userPermissionDao().selectGlobalPermissionsOfUser(dbSession, user.getId(), organization.getUuid()))
-      .containsOnly(GlobalPermissions.ALL.toArray(new String[GlobalPermissions.ALL.size()]));
-  }
-
-  @Test
-  public void createForUser_creates_members_group_and_add_current_user_to_it() {
-    UserDto user = db.users().insertUser(dto -> dto.setLogin(A_LOGIN).setName(A_NAME));
-    when(organizationValidation.generateKeyFrom(A_LOGIN)).thenReturn(SLUG_OF_A_LOGIN);
-    enableCreatePersonalOrg(true);
-    builtInQProfileRepositoryRule.initialize();
-    db.qualityGates().insertBuiltInQualityGate();
-
-    underTest.createForUser(dbSession, user);
-
-    verifyMembersGroup(user, SLUG_OF_A_LOGIN);
-  }
-
-  @Test
-  public void createForUser_creates_default_template_for_new_organization() {
-    UserDto user = db.users().insertUser(dto -> dto.setLogin(A_LOGIN).setName(A_NAME));
-    when(organizationValidation.generateKeyFrom(A_LOGIN)).thenReturn(SLUG_OF_A_LOGIN);
-    enableCreatePersonalOrg(true);
-    builtInQProfileRepositoryRule.initialize();
-    db.qualityGates().insertBuiltInQualityGate();
-
-    underTest.createForUser(dbSession, user);
-
-    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, SLUG_OF_A_LOGIN).get();
-    int defaultGroupId = dbClient.organizationDao().getDefaultGroupId(dbSession, organization.getUuid()).get();
-    PermissionTemplateDto defaultTemplate = dbClient.permissionTemplateDao().selectByName(dbSession, organization.getUuid(), "default template");
-    assertThat(defaultTemplate.getName()).isEqualTo("Default template");
-    assertThat(defaultTemplate.getDescription()).isEqualTo("Default permission template of organization " + A_NAME);
-    DefaultTemplates defaultTemplates = dbClient.organizationDao().getDefaultTemplates(dbSession, organization.getUuid()).get();
-    assertThat(defaultTemplates.getProjectUuid()).isEqualTo(defaultTemplate.getUuid());
-    assertThat(defaultTemplates.getViewUuid()).isNull();
-    assertThat(dbClient.permissionTemplateDao().selectGroupPermissionsByTemplateId(dbSession, defaultTemplate.getId()))
-      .extracting(PermissionTemplateGroupDto::getGroupId, PermissionTemplateGroupDto::getPermission)
-      .containsOnly(
-        tuple(defaultGroupId, UserRole.USER), tuple(defaultGroupId, UserRole.CODEVIEWER));
-    assertThat(dbClient.permissionTemplateCharacteristicDao().selectByTemplateIds(dbSession, Collections.singletonList(defaultTemplate.getId())))
-      .extracting(PermissionTemplateCharacteristicDto::getWithProjectCreator, PermissionTemplateCharacteristicDto::getPermission)
-      .containsOnly(
-        tuple(true, UserRole.ADMIN), tuple(true, UserRole.ISSUE_ADMIN), tuple(true, GlobalPermissions.SCAN_EXECUTION));
-  }
-
-  @Test
-  public void createForUser_add_current_user_as_member_of_organization() {
-    UserDto user = db.users().insertUser(dto -> dto.setLogin(A_LOGIN).setName(A_NAME));
-    when(organizationValidation.generateKeyFrom(A_LOGIN)).thenReturn(SLUG_OF_A_LOGIN);
-    enableCreatePersonalOrg(true);
-    builtInQProfileRepositoryRule.initialize();
-    db.qualityGates().insertBuiltInQualityGate();
-
-    underTest.createForUser(dbSession, user);
-
-    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, SLUG_OF_A_LOGIN).get();
-    assertThat(dbClient.organizationMemberDao().select(dbSession, organization.getUuid(), user.getId())).isPresent();
-  }
-
-  @Test
-  public void createForUser_does_not_fail_if_name_is_too_long_for_an_organization_name() {
-    String nameTooLong = STRING_64_CHARS + "b";
-    UserDto user = db.users().insertUser(dto -> dto.setName(nameTooLong).setLogin(A_LOGIN));
-    when(organizationValidation.generateKeyFrom(A_LOGIN)).thenReturn(SLUG_OF_A_LOGIN);
-    enableCreatePersonalOrg(true);
-    builtInQProfileRepositoryRule.initialize();
-    db.qualityGates().insertBuiltInQualityGate();
-
-    underTest.createForUser(dbSession, user);
-
-    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, SLUG_OF_A_LOGIN).get();
-    assertThat(organization.getName()).isEqualTo(STRING_64_CHARS);
-    assertThat(organization.getDescription()).isEqualTo(nameTooLong + "'s personal organization");
-  }
-
-  @Test
-  public void createForUser_does_not_fail_if_name_is_empty_and_login_is_too_long_for_an_organization_name() {
-    String login = STRING_64_CHARS + "b";
-    UserDto user = db.users().insertUser(dto -> dto.setName("").setLogin(login));
-    when(organizationValidation.generateKeyFrom(login)).thenReturn(SLUG_OF_A_LOGIN);
-    enableCreatePersonalOrg(true);
-    builtInQProfileRepositoryRule.initialize();
-    db.qualityGates().insertBuiltInQualityGate();
-
-    underTest.createForUser(dbSession, user);
-
-    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, SLUG_OF_A_LOGIN).get();
-    assertThat(organization.getName()).isEqualTo(STRING_64_CHARS);
-    assertThat(organization.getDescription()).isEqualTo(login + "'s personal organization");
-  }
-
-  @Test
-  public void createForUser_does_not_fail_if_name_is_null_and_login_is_too_long_for_an_organization_name() {
-    String login = STRING_64_CHARS + "b";
-    UserDto user = db.users().insertUser(dto -> dto.setName(null).setLogin(login));
-    when(organizationValidation.generateKeyFrom(login)).thenReturn(SLUG_OF_A_LOGIN);
-    enableCreatePersonalOrg(true);
-    builtInQProfileRepositoryRule.initialize();
-    db.qualityGates().insertBuiltInQualityGate();
-
-    underTest.createForUser(dbSession, user);
-
-    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, SLUG_OF_A_LOGIN).get();
-    assertThat(organization.getName()).isEqualTo(STRING_64_CHARS);
-    assertThat(organization.getDescription()).isEqualTo(login + "'s personal organization");
-  }
-
-  @Test
-  public void createForUser_associates_to_built_in_quality_profiles() {
-    UserDto user = db.users().insertUser(A_LOGIN);
-    when(organizationValidation.generateKeyFrom(A_LOGIN)).thenReturn(SLUG_OF_A_LOGIN);
-    enableCreatePersonalOrg(true);
-    db.qualityGates().insertBuiltInQualityGate();
-    BuiltInQProfile builtIn1 = builtInQProfileRepositoryRule.add(newLanguage("foo"), "qp1");
-    BuiltInQProfile builtIn2 = builtInQProfileRepositoryRule.add(newLanguage("foo"), "qp2");
-    builtInQProfileRepositoryRule.initialize();
-    insertRulesProfile(builtIn1);
-    insertRulesProfile(builtIn2);
-
-    underTest.createForUser(dbSession, user);
-
-    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, SLUG_OF_A_LOGIN).get();
-    List<QProfileDto> profiles = dbClient.qualityProfileDao().selectOrderedByOrganizationUuid(dbSession, organization);
-    assertThat(profiles).extracting(p -> new QProfileName(p.getLanguage(), p.getName())).containsExactlyInAnyOrder(
-      builtIn1.getQProfileName(), builtIn2.getQProfileName());
-  }
-
-  @Test
-  public void createForUser_associates_to_built_in_quality_gate() {
-    QualityGateDto builtInQualityGate = db.qualityGates().insertBuiltInQualityGate();
-    UserDto user = db.users().insertUser(A_LOGIN);
-    when(organizationValidation.generateKeyFrom(A_LOGIN)).thenReturn(SLUG_OF_A_LOGIN);
-    enableCreatePersonalOrg(true);
-    builtInQProfileRepositoryRule.initialize();
-
-    underTest.createForUser(dbSession, user);
-
-    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, SLUG_OF_A_LOGIN).get();
-    assertThat(dbClient.qualityGateDao().selectDefault(dbSession, organization).getUuid()).isEqualTo(builtInQualityGate.getUuid());
-  }
-
-  private void enableCreatePersonalOrg(boolean flag) {
-    settings.setProperty(CorePropertyDefinitions.ORGANIZATIONS_CREATE_PERSONAL_ORG, flag);
-  }
-
-  private void verifyGroupOwners(UserDto user, String organizationKey, String organizationName) {
-    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, organizationKey).get();
-    Optional<GroupDto> groupOpt = dbClient.groupDao().selectByName(dbSession, organization.getUuid(), "Owners");
-    assertThat(groupOpt).isPresent();
-    GroupDto groupDto = groupOpt.get();
-    assertThat(groupDto.getDescription()).isEqualTo("Owners of organization " + organizationName);
-
-    assertThat(dbClient.groupPermissionDao().selectGlobalPermissionsOfGroup(dbSession, groupDto.getOrganizationUuid(), groupDto.getId()))
-      .containsOnly(GlobalPermissions.ALL.toArray(new String[GlobalPermissions.ALL.size()]));
-    List<UserMembershipDto> members = dbClient.groupMembershipDao().selectMembers(
-      dbSession,
-      UserMembershipQuery.builder()
-        .organizationUuid(organization.getUuid())
-        .groupId(groupDto.getId())
-        .membership(UserMembershipQuery.IN).build(),
-      0, Integer.MAX_VALUE);
-    assertThat(members)
-      .extracting(UserMembershipDto::getLogin)
-      .containsOnly(user.getLogin());
-  }
-
-  private void verifyMembersGroup(UserDto user, String organizationKey) {
-    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, organizationKey).get();
-    Optional<GroupDto> groupOpt = dbClient.groupDao().selectByName(dbSession, organization.getUuid(), "Members");
-    assertThat(groupOpt).isPresent();
-    GroupDto groupDto = groupOpt.get();
-    assertThat(groupDto.getDescription()).isEqualTo("All members of the organization");
-
-    assertThat(dbClient.groupPermissionDao().selectGlobalPermissionsOfGroup(dbSession, groupDto.getOrganizationUuid(), groupDto.getId())).isEmpty();
-    List<UserMembershipDto> members = dbClient.groupMembershipDao().selectMembers(
-      dbSession,
-      UserMembershipQuery.builder()
-        .organizationUuid(organization.getUuid())
-        .groupId(groupDto.getId())
-        .membership(UserMembershipQuery.IN).build(),
-      0, Integer.MAX_VALUE);
-    assertThat(members)
-      .extracting(UserMembershipDto::getLogin)
-      .containsOnly(user.getLogin());
-  }
-
-}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/organization/OrganizationUpdaterImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/organization/OrganizationUpdaterImplTest.java
new file mode 100644 (file)
index 0000000..9e08bc9
--- /dev/null
@@ -0,0 +1,626 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.organization;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import org.apache.commons.lang.RandomStringUtils;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.internal.TestSystem2;
+import org.sonar.api.web.UserRole;
+import org.sonar.core.config.CorePropertyDefinitions;
+import org.sonar.core.permission.GlobalPermissions;
+import org.sonar.core.util.SequenceUuidFactory;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.organization.DefaultTemplates;
+import org.sonar.db.organization.OrganizationDto;
+import org.sonar.db.permission.template.PermissionTemplateCharacteristicDto;
+import org.sonar.db.permission.template.PermissionTemplateDto;
+import org.sonar.db.permission.template.PermissionTemplateGroupDto;
+import org.sonar.db.qualitygate.QualityGateDto;
+import org.sonar.db.qualityprofile.QProfileDto;
+import org.sonar.db.qualityprofile.RulesProfileDto;
+import org.sonar.db.user.GroupDto;
+import org.sonar.db.user.UserDto;
+import org.sonar.db.user.UserMembershipDto;
+import org.sonar.db.user.UserMembershipQuery;
+import org.sonar.server.es.EsTester;
+import org.sonar.server.es.SearchOptions;
+import org.sonar.server.qualityprofile.BuiltInQProfile;
+import org.sonar.server.qualityprofile.BuiltInQProfileRepositoryRule;
+import org.sonar.server.qualityprofile.QProfileName;
+import org.sonar.server.user.index.UserIndex;
+import org.sonar.server.user.index.UserIndexer;
+import org.sonar.server.user.index.UserQuery;
+import org.sonar.server.usergroups.DefaultGroupCreator;
+import org.sonar.server.usergroups.DefaultGroupCreatorImpl;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+import static org.assertj.core.api.Assertions.tuple;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.server.language.LanguageTesting.newLanguage;
+import static org.sonar.server.organization.OrganizationUpdater.NewOrganization.newOrganizationBuilder;
+
+public class OrganizationUpdaterImplTest {
+  private static final long A_DATE = 12893434L;
+  private static final String A_LOGIN = "a-login";
+  private static final String SLUG_OF_A_LOGIN = "slug-of-a-login";
+  private static final String STRING_64_CHARS = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
+  private static final String A_NAME = "a name";
+
+  private OrganizationUpdater.NewOrganization FULL_POPULATED_NEW_ORGANIZATION = newOrganizationBuilder()
+    .setName("a-name")
+    .setKey("a-key")
+    .setDescription("a-description")
+    .setUrl("a-url")
+    .setAvatarUrl("a-avatar")
+    .build();
+
+  private System2 system2 = new TestSystem2().setNow(A_DATE);
+
+  @Rule
+  public DbTester db = DbTester.create(system2);
+  @Rule
+  public EsTester es = EsTester.create();
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+  @Rule
+  public BuiltInQProfileRepositoryRule builtInQProfileRepositoryRule = new BuiltInQProfileRepositoryRule();
+
+  private DbSession dbSession = db.getSession();
+
+  private IllegalArgumentException exceptionThrownByOrganizationValidation = new IllegalArgumentException("simulate IAE thrown by OrganizationValidation");
+  private DbClient dbClient = db.getDbClient();
+  private UuidFactory uuidFactory = new SequenceUuidFactory();
+  private OrganizationValidation organizationValidation = mock(OrganizationValidation.class);
+  private MapSettings settings = new MapSettings();
+  private UserIndexer userIndexer = new UserIndexer(dbClient, es.client());
+  private UserIndex userIndex = new UserIndex(es.client(), system2);
+  private DefaultGroupCreator defaultGroupCreator = new DefaultGroupCreatorImpl(dbClient);
+  private OrganizationUpdaterImpl underTest = new OrganizationUpdaterImpl(dbClient, system2, uuidFactory, organizationValidation, settings.asConfig(), userIndexer,
+    builtInQProfileRepositoryRule, defaultGroupCreator);
+
+  @Test
+  public void create_throws_NPE_if_NewOrganization_arg_is_null() throws OrganizationUpdater.KeyConflictException {
+    UserDto user = db.users().insertUser();
+
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("newOrganization can't be null");
+
+    underTest.create(dbSession, user, null);
+  }
+
+  @Test
+  public void create_throws_exception_thrown_by_checkValidKey() throws OrganizationUpdater.KeyConflictException {
+    UserDto user = db.users().insertUser();
+
+    when(organizationValidation.checkKey(FULL_POPULATED_NEW_ORGANIZATION.getKey()))
+      .thenThrow(exceptionThrownByOrganizationValidation);
+
+    createThrowsExceptionThrownByOrganizationValidation(user);
+  }
+
+  @Test
+  public void create_throws_exception_thrown_by_checkValidDescription() throws OrganizationUpdater.KeyConflictException {
+    UserDto user = db.users().insertUser();
+
+    when(organizationValidation.checkDescription(FULL_POPULATED_NEW_ORGANIZATION.getDescription())).thenThrow(exceptionThrownByOrganizationValidation);
+
+    createThrowsExceptionThrownByOrganizationValidation(user);
+  }
+
+  @Test
+  public void create_throws_exception_thrown_by_checkValidUrl() throws OrganizationUpdater.KeyConflictException {
+    UserDto user = db.users().insertUser();
+
+    when(organizationValidation.checkUrl(FULL_POPULATED_NEW_ORGANIZATION.getUrl())).thenThrow(exceptionThrownByOrganizationValidation);
+
+    createThrowsExceptionThrownByOrganizationValidation(user);
+  }
+
+  @Test
+  public void create_throws_exception_thrown_by_checkValidAvatar() throws OrganizationUpdater.KeyConflictException {
+    UserDto user = db.users().insertUser();
+
+    when(organizationValidation.checkAvatar(FULL_POPULATED_NEW_ORGANIZATION.getAvatar())).thenThrow(exceptionThrownByOrganizationValidation);
+
+    createThrowsExceptionThrownByOrganizationValidation(user);
+  }
+
+  private void createThrowsExceptionThrownByOrganizationValidation(UserDto user) throws OrganizationUpdater.KeyConflictException {
+    try {
+      underTest.create(dbSession, user, FULL_POPULATED_NEW_ORGANIZATION);
+      fail(exceptionThrownByOrganizationValidation + " should have been thrown");
+    } catch (IllegalArgumentException e) {
+      assertThat(e).isSameAs(exceptionThrownByOrganizationValidation);
+    }
+  }
+
+  @Test
+  public void create_fails_with_ISE_if_BuiltInQProfileRepository_has_not_been_initialized() throws OrganizationUpdater.KeyConflictException {
+    UserDto user = db.users().insertUser();
+    db.qualityGates().insertBuiltInQualityGate();
+
+    expectedException.expect(IllegalStateException.class);
+    expectedException.expectMessage("initialize must be called first");
+
+    underTest.create(dbSession, user, FULL_POPULATED_NEW_ORGANIZATION);
+  }
+
+  @Test
+  public void create_fails_with_KeyConflictException_if_org_with_key_in_NewOrganization_arg_already_exists_in_db() throws OrganizationUpdater.KeyConflictException {
+    db.organizations().insertForKey(FULL_POPULATED_NEW_ORGANIZATION.getKey());
+    UserDto user = db.users().insertUser();
+
+    expectedException.expect(OrganizationUpdater.KeyConflictException.class);
+    expectedException.expectMessage("Organization key '" + FULL_POPULATED_NEW_ORGANIZATION.getKey() + "' is already used");
+
+    underTest.create(dbSession, user, FULL_POPULATED_NEW_ORGANIZATION);
+  }
+
+  @Test
+  public void create_creates_unguarded_organization_with_properties_from_NewOrganization_arg() throws OrganizationUpdater.KeyConflictException {
+    builtInQProfileRepositoryRule.initialize();
+    UserDto user = db.users().insertUser();
+    db.qualityGates().insertBuiltInQualityGate();
+
+    underTest.create(dbSession, user, FULL_POPULATED_NEW_ORGANIZATION);
+
+    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, FULL_POPULATED_NEW_ORGANIZATION.getKey()).get();
+    assertThat(organization.getUuid()).isNotEmpty();
+    assertThat(organization.getKey()).isEqualTo(FULL_POPULATED_NEW_ORGANIZATION.getKey());
+    assertThat(organization.getName()).isEqualTo(FULL_POPULATED_NEW_ORGANIZATION.getName());
+    assertThat(organization.getDescription()).isEqualTo(FULL_POPULATED_NEW_ORGANIZATION.getDescription());
+    assertThat(organization.getUrl()).isEqualTo(FULL_POPULATED_NEW_ORGANIZATION.getUrl());
+    assertThat(organization.getAvatarUrl()).isEqualTo(FULL_POPULATED_NEW_ORGANIZATION.getAvatar());
+    assertThat(organization.isGuarded()).isFalse();
+    assertThat(organization.getCreatedAt()).isEqualTo(A_DATE);
+    assertThat(organization.getUpdatedAt()).isEqualTo(A_DATE);
+  }
+
+  @Test
+  public void create_creates_owners_group_with_all_permissions_for_new_organization_and_add_current_user_to_it() throws OrganizationUpdater.KeyConflictException {
+    UserDto user = db.users().insertUser();
+    builtInQProfileRepositoryRule.initialize();
+    db.qualityGates().insertBuiltInQualityGate();
+
+    underTest.create(dbSession, user, FULL_POPULATED_NEW_ORGANIZATION);
+
+    verifyGroupOwners(user, FULL_POPULATED_NEW_ORGANIZATION.getKey(), FULL_POPULATED_NEW_ORGANIZATION.getName());
+  }
+
+  @Test
+  public void create_creates_members_group_and_add_current_user_to_it() throws OrganizationUpdater.KeyConflictException {
+    UserDto user = db.users().insertUser();
+    builtInQProfileRepositoryRule.initialize();
+    db.qualityGates().insertBuiltInQualityGate();
+
+    underTest.create(dbSession, user, FULL_POPULATED_NEW_ORGANIZATION);
+
+    verifyMembersGroup(user, FULL_POPULATED_NEW_ORGANIZATION.getKey());
+  }
+
+  @Test
+  public void create_does_not_require_description_url_and_avatar_to_be_non_null() throws OrganizationUpdater.KeyConflictException {
+    builtInQProfileRepositoryRule.initialize();
+    UserDto user = db.users().insertUser();
+    db.qualityGates().insertBuiltInQualityGate();
+
+    underTest.create(dbSession, user, newOrganizationBuilder()
+      .setKey("key")
+      .setName("name")
+      .build());
+
+    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, "key").get();
+    assertThat(organization.getKey()).isEqualTo("key");
+    assertThat(organization.getName()).isEqualTo("name");
+    assertThat(organization.getDescription()).isNull();
+    assertThat(organization.getUrl()).isNull();
+    assertThat(organization.getAvatarUrl()).isNull();
+    assertThat(organization.isGuarded()).isFalse();
+  }
+
+  @Test
+  public void create_creates_default_template_for_new_organization() throws OrganizationUpdater.KeyConflictException {
+    builtInQProfileRepositoryRule.initialize();
+    UserDto user = db.users().insertUser();
+    db.qualityGates().insertBuiltInQualityGate();
+
+    underTest.create(dbSession, user, FULL_POPULATED_NEW_ORGANIZATION);
+
+    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, FULL_POPULATED_NEW_ORGANIZATION.getKey()).get();
+    GroupDto ownersGroup = dbClient.groupDao().selectByName(dbSession, organization.getUuid(), "Owners").get();
+    int defaultGroupId = dbClient.organizationDao().getDefaultGroupId(dbSession, organization.getUuid()).get();
+    PermissionTemplateDto defaultTemplate = dbClient.permissionTemplateDao().selectByName(dbSession, organization.getUuid(), "default template");
+    assertThat(defaultTemplate.getName()).isEqualTo("Default template");
+    assertThat(defaultTemplate.getDescription()).isEqualTo("Default permission template of organization " + FULL_POPULATED_NEW_ORGANIZATION.getName());
+    DefaultTemplates defaultTemplates = dbClient.organizationDao().getDefaultTemplates(dbSession, organization.getUuid()).get();
+    assertThat(defaultTemplates.getProjectUuid()).isEqualTo(defaultTemplate.getUuid());
+    assertThat(defaultTemplates.getViewUuid()).isNull();
+    assertThat(dbClient.permissionTemplateDao().selectGroupPermissionsByTemplateId(dbSession, defaultTemplate.getId()))
+      .extracting(PermissionTemplateGroupDto::getGroupId, PermissionTemplateGroupDto::getPermission)
+      .containsOnly(
+        tuple(ownersGroup.getId(), UserRole.ADMIN), tuple(ownersGroup.getId(), UserRole.ISSUE_ADMIN), tuple(ownersGroup.getId(), GlobalPermissions.SCAN_EXECUTION),
+        tuple(defaultGroupId, UserRole.USER), tuple(defaultGroupId, UserRole.CODEVIEWER));
+  }
+
+  @Test
+  public void create_add_current_user_as_member_of_organization() throws OrganizationUpdater.KeyConflictException {
+    UserDto user = db.users().insertUser();
+    builtInQProfileRepositoryRule.initialize();
+    db.qualityGates().insertBuiltInQualityGate();
+
+    OrganizationDto result = underTest.create(dbSession, user, FULL_POPULATED_NEW_ORGANIZATION);
+
+    assertThat(dbClient.organizationMemberDao().select(dbSession, result.getUuid(), user.getId())).isPresent();
+    assertThat(userIndex.search(UserQuery.builder().setOrganizationUuid(result.getUuid()).setTextQuery(user.getLogin()).build(), new SearchOptions()).getTotal()).isEqualTo(1L);
+  }
+
+  @Test
+  public void create_associates_to_built_in_quality_profiles() throws OrganizationUpdater.KeyConflictException {
+    BuiltInQProfile builtIn1 = builtInQProfileRepositoryRule.add(newLanguage("foo"), "qp1", true);
+    BuiltInQProfile builtIn2 = builtInQProfileRepositoryRule.add(newLanguage("foo"), "qp2");
+    builtInQProfileRepositoryRule.initialize();
+    insertRulesProfile(builtIn1);
+    insertRulesProfile(builtIn2);
+    UserDto user = db.users().insertUser();
+    db.qualityGates().insertBuiltInQualityGate();
+
+    underTest.create(dbSession, user, FULL_POPULATED_NEW_ORGANIZATION);
+
+    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, FULL_POPULATED_NEW_ORGANIZATION.getKey()).get();
+    List<QProfileDto> profiles = dbClient.qualityProfileDao().selectOrderedByOrganizationUuid(dbSession, organization);
+    assertThat(profiles).extracting(p -> new QProfileName(p.getLanguage(), p.getName())).containsExactlyInAnyOrder(
+      builtIn1.getQProfileName(), builtIn2.getQProfileName());
+    assertThat(dbClient.qualityProfileDao().selectDefaultProfile(dbSession, organization, "foo").getName())
+      .isEqualTo("qp1");
+  }
+
+  private void insertRulesProfile(BuiltInQProfile builtIn) {
+    RulesProfileDto dto = new RulesProfileDto()
+      .setIsBuiltIn(true)
+      .setKee(RandomStringUtils.randomAlphabetic(40))
+      .setLanguage(builtIn.getLanguage())
+      .setName(builtIn.getName());
+    dbClient.qualityProfileDao().insert(db.getSession(), dto);
+    db.commit();
+  }
+
+  @Test
+  public void create_associates_to_built_in_quality_gate() throws OrganizationUpdater.KeyConflictException {
+    QualityGateDto builtInQualityGate = db.qualityGates().insertBuiltInQualityGate();
+    builtInQProfileRepositoryRule.initialize();
+    UserDto user = db.users().insertUser();
+
+    underTest.create(dbSession, user, FULL_POPULATED_NEW_ORGANIZATION);
+
+    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, FULL_POPULATED_NEW_ORGANIZATION.getKey()).get();
+    assertThat(dbClient.qualityGateDao().selectDefault(dbSession, organization).getUuid()).isEqualTo(builtInQualityGate.getUuid());
+  }
+
+  @Test
+  public void createForUser_has_no_effect_if_setting_for_feature_is_not_set() {
+    checkSizeOfTables();
+
+    underTest.createForUser(null /* argument is not even read */, null /* argument is not even read */);
+
+    checkSizeOfTables();
+  }
+
+  @Test
+  public void createForUser_has_no_effect_if_setting_for_feature_is_disabled() {
+    enableCreatePersonalOrg(false);
+
+    checkSizeOfTables();
+
+    underTest.createForUser(null /* argument is not even read */, null /* argument is not even read */);
+
+    checkSizeOfTables();
+  }
+
+  private void checkSizeOfTables() {
+    assertThat(db.countRowsOfTable("organizations")).isEqualTo(1);
+    assertThat(db.countRowsOfTable("groups")).isEqualTo(0);
+    assertThat(db.countRowsOfTable("groups_users")).isEqualTo(0);
+    assertThat(db.countRowsOfTable("permission_templates")).isEqualTo(0);
+    assertThat(db.countRowsOfTable("perm_templates_users")).isEqualTo(0);
+    assertThat(db.countRowsOfTable("perm_templates_groups")).isEqualTo(0);
+  }
+
+  @Test
+  public void createForUser_creates_guarded_organization_with_key_name_and_description_generated_from_user_login_and_name_and_associated_to_user() {
+    UserDto user = db.users().insertUser(A_LOGIN);
+    when(organizationValidation.generateKeyFrom(A_LOGIN)).thenReturn(SLUG_OF_A_LOGIN);
+    enableCreatePersonalOrg(true);
+    builtInQProfileRepositoryRule.initialize();
+    db.qualityGates().insertBuiltInQualityGate();
+
+    underTest.createForUser(dbSession, user);
+
+    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, SLUG_OF_A_LOGIN).get();
+    assertThat(organization.getUuid()).isNotEmpty();
+    assertThat(organization.getKey()).isEqualTo(SLUG_OF_A_LOGIN);
+    assertThat(organization.getName()).isEqualTo(user.getName());
+    assertThat(organization.getDescription()).isEqualTo(user.getName() + "'s personal organization");
+    assertThat(organization.getUrl()).isNull();
+    assertThat(organization.getAvatarUrl()).isNull();
+    assertThat(organization.isGuarded()).isTrue();
+    assertThat(organization.getCreatedAt()).isEqualTo(A_DATE);
+    assertThat(organization.getUpdatedAt()).isEqualTo(A_DATE);
+
+    assertThat(db.getDbClient().userDao().selectByUuid(dbSession, user.getUuid()).getOrganizationUuid()).isEqualTo(organization.getUuid());
+  }
+
+  @Test
+  public void createForUser_fails_with_ISE_if_organization_with_slug_of_login_already_exists() {
+    UserDto user = db.users().insertUser(A_LOGIN);
+    when(organizationValidation.generateKeyFrom(A_LOGIN)).thenReturn(SLUG_OF_A_LOGIN);
+    db.organizations().insertForKey(SLUG_OF_A_LOGIN);
+    enableCreatePersonalOrg(true);
+
+    expectedException.expect(IllegalStateException.class);
+    expectedException.expectMessage("Can't create organization with key '" + SLUG_OF_A_LOGIN + "' " +
+      "because an organization with this key already exists");
+
+    underTest.createForUser(dbSession, user);
+  }
+
+  @Test
+  public void createForUser_gives_all_permissions_for_new_organization_to_current_user() {
+    UserDto user = db.users().insertUser(dto -> dto.setLogin(A_LOGIN).setName(A_NAME));
+    when(organizationValidation.generateKeyFrom(A_LOGIN)).thenReturn(SLUG_OF_A_LOGIN);
+    enableCreatePersonalOrg(true);
+    builtInQProfileRepositoryRule.initialize();
+    db.qualityGates().insertBuiltInQualityGate();
+
+    underTest.createForUser(dbSession, user);
+
+    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, SLUG_OF_A_LOGIN).get();
+    assertThat(dbClient.userPermissionDao().selectGlobalPermissionsOfUser(dbSession, user.getId(), organization.getUuid()))
+      .containsOnly(GlobalPermissions.ALL.toArray(new String[GlobalPermissions.ALL.size()]));
+  }
+
+  @Test
+  public void createForUser_creates_members_group_and_add_current_user_to_it() {
+    UserDto user = db.users().insertUser(dto -> dto.setLogin(A_LOGIN).setName(A_NAME));
+    when(organizationValidation.generateKeyFrom(A_LOGIN)).thenReturn(SLUG_OF_A_LOGIN);
+    enableCreatePersonalOrg(true);
+    builtInQProfileRepositoryRule.initialize();
+    db.qualityGates().insertBuiltInQualityGate();
+
+    underTest.createForUser(dbSession, user);
+
+    verifyMembersGroup(user, SLUG_OF_A_LOGIN);
+  }
+
+  @Test
+  public void createForUser_creates_default_template_for_new_organization() {
+    UserDto user = db.users().insertUser(dto -> dto.setLogin(A_LOGIN).setName(A_NAME));
+    when(organizationValidation.generateKeyFrom(A_LOGIN)).thenReturn(SLUG_OF_A_LOGIN);
+    enableCreatePersonalOrg(true);
+    builtInQProfileRepositoryRule.initialize();
+    db.qualityGates().insertBuiltInQualityGate();
+
+    underTest.createForUser(dbSession, user);
+
+    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, SLUG_OF_A_LOGIN).get();
+    int defaultGroupId = dbClient.organizationDao().getDefaultGroupId(dbSession, organization.getUuid()).get();
+    PermissionTemplateDto defaultTemplate = dbClient.permissionTemplateDao().selectByName(dbSession, organization.getUuid(), "default template");
+    assertThat(defaultTemplate.getName()).isEqualTo("Default template");
+    assertThat(defaultTemplate.getDescription()).isEqualTo("Default permission template of organization " + A_NAME);
+    DefaultTemplates defaultTemplates = dbClient.organizationDao().getDefaultTemplates(dbSession, organization.getUuid()).get();
+    assertThat(defaultTemplates.getProjectUuid()).isEqualTo(defaultTemplate.getUuid());
+    assertThat(defaultTemplates.getViewUuid()).isNull();
+    assertThat(dbClient.permissionTemplateDao().selectGroupPermissionsByTemplateId(dbSession, defaultTemplate.getId()))
+      .extracting(PermissionTemplateGroupDto::getGroupId, PermissionTemplateGroupDto::getPermission)
+      .containsOnly(
+        tuple(defaultGroupId, UserRole.USER), tuple(defaultGroupId, UserRole.CODEVIEWER));
+    assertThat(dbClient.permissionTemplateCharacteristicDao().selectByTemplateIds(dbSession, Collections.singletonList(defaultTemplate.getId())))
+      .extracting(PermissionTemplateCharacteristicDto::getWithProjectCreator, PermissionTemplateCharacteristicDto::getPermission)
+      .containsOnly(
+        tuple(true, UserRole.ADMIN), tuple(true, UserRole.ISSUE_ADMIN), tuple(true, GlobalPermissions.SCAN_EXECUTION));
+  }
+
+  @Test
+  public void createForUser_add_current_user_as_member_of_organization() {
+    UserDto user = db.users().insertUser(dto -> dto.setLogin(A_LOGIN).setName(A_NAME));
+    when(organizationValidation.generateKeyFrom(A_LOGIN)).thenReturn(SLUG_OF_A_LOGIN);
+    enableCreatePersonalOrg(true);
+    builtInQProfileRepositoryRule.initialize();
+    db.qualityGates().insertBuiltInQualityGate();
+
+    underTest.createForUser(dbSession, user);
+
+    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, SLUG_OF_A_LOGIN).get();
+    assertThat(dbClient.organizationMemberDao().select(dbSession, organization.getUuid(), user.getId())).isPresent();
+  }
+
+  @Test
+  public void createForUser_does_not_fail_if_name_is_too_long_for_an_organization_name() {
+    String nameTooLong = STRING_64_CHARS + "b";
+    UserDto user = db.users().insertUser(dto -> dto.setName(nameTooLong).setLogin(A_LOGIN));
+    when(organizationValidation.generateKeyFrom(A_LOGIN)).thenReturn(SLUG_OF_A_LOGIN);
+    enableCreatePersonalOrg(true);
+    builtInQProfileRepositoryRule.initialize();
+    db.qualityGates().insertBuiltInQualityGate();
+
+    underTest.createForUser(dbSession, user);
+
+    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, SLUG_OF_A_LOGIN).get();
+    assertThat(organization.getName()).isEqualTo(STRING_64_CHARS);
+    assertThat(organization.getDescription()).isEqualTo(nameTooLong + "'s personal organization");
+  }
+
+  @Test
+  public void createForUser_does_not_fail_if_name_is_empty_and_login_is_too_long_for_an_organization_name() {
+    String login = STRING_64_CHARS + "b";
+    UserDto user = db.users().insertUser(dto -> dto.setName("").setLogin(login));
+    when(organizationValidation.generateKeyFrom(login)).thenReturn(SLUG_OF_A_LOGIN);
+    enableCreatePersonalOrg(true);
+    builtInQProfileRepositoryRule.initialize();
+    db.qualityGates().insertBuiltInQualityGate();
+
+    underTest.createForUser(dbSession, user);
+
+    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, SLUG_OF_A_LOGIN).get();
+    assertThat(organization.getName()).isEqualTo(STRING_64_CHARS);
+    assertThat(organization.getDescription()).isEqualTo(login + "'s personal organization");
+  }
+
+  @Test
+  public void createForUser_does_not_fail_if_name_is_null_and_login_is_too_long_for_an_organization_name() {
+    String login = STRING_64_CHARS + "b";
+    UserDto user = db.users().insertUser(dto -> dto.setName(null).setLogin(login));
+    when(organizationValidation.generateKeyFrom(login)).thenReturn(SLUG_OF_A_LOGIN);
+    enableCreatePersonalOrg(true);
+    builtInQProfileRepositoryRule.initialize();
+    db.qualityGates().insertBuiltInQualityGate();
+
+    underTest.createForUser(dbSession, user);
+
+    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, SLUG_OF_A_LOGIN).get();
+    assertThat(organization.getName()).isEqualTo(STRING_64_CHARS);
+    assertThat(organization.getDescription()).isEqualTo(login + "'s personal organization");
+  }
+
+  @Test
+  public void createForUser_associates_to_built_in_quality_profiles() {
+    UserDto user = db.users().insertUser(A_LOGIN);
+    when(organizationValidation.generateKeyFrom(A_LOGIN)).thenReturn(SLUG_OF_A_LOGIN);
+    enableCreatePersonalOrg(true);
+    db.qualityGates().insertBuiltInQualityGate();
+    BuiltInQProfile builtIn1 = builtInQProfileRepositoryRule.add(newLanguage("foo"), "qp1");
+    BuiltInQProfile builtIn2 = builtInQProfileRepositoryRule.add(newLanguage("foo"), "qp2");
+    builtInQProfileRepositoryRule.initialize();
+    insertRulesProfile(builtIn1);
+    insertRulesProfile(builtIn2);
+
+    underTest.createForUser(dbSession, user);
+
+    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, SLUG_OF_A_LOGIN).get();
+    List<QProfileDto> profiles = dbClient.qualityProfileDao().selectOrderedByOrganizationUuid(dbSession, organization);
+    assertThat(profiles).extracting(p -> new QProfileName(p.getLanguage(), p.getName())).containsExactlyInAnyOrder(
+      builtIn1.getQProfileName(), builtIn2.getQProfileName());
+  }
+
+  @Test
+  public void createForUser_associates_to_built_in_quality_gate() {
+    QualityGateDto builtInQualityGate = db.qualityGates().insertBuiltInQualityGate();
+    UserDto user = db.users().insertUser(A_LOGIN);
+    when(organizationValidation.generateKeyFrom(A_LOGIN)).thenReturn(SLUG_OF_A_LOGIN);
+    enableCreatePersonalOrg(true);
+    builtInQProfileRepositoryRule.initialize();
+
+    underTest.createForUser(dbSession, user);
+
+    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, SLUG_OF_A_LOGIN).get();
+    assertThat(dbClient.qualityGateDao().selectDefault(dbSession, organization).getUuid()).isEqualTo(builtInQualityGate.getUuid());
+  }
+
+  @Test
+  public void update_personal_organization() {
+    OrganizationDto organization = db.organizations().insert(o -> o.setKey("old login"));
+    when(organizationValidation.generateKeyFrom("new_login")).thenReturn("new_login");
+
+    underTest.updateOrganizationKey(dbSession, organization, "new_login");
+
+    OrganizationDto organizationReloaded = dbClient.organizationDao().selectByUuid(dbSession, organization.getUuid()).get();
+    assertThat(organizationReloaded.getKey()).isEqualTo("new_login");
+  }
+
+  @Test
+  public void does_not_update_personal_organization_when_generated_organization_key_does_not_change() {
+    OrganizationDto organization = db.organizations().insert(o -> o.setKey("login"));
+    when(organizationValidation.generateKeyFrom("Login")).thenReturn("login");
+
+    underTest.updateOrganizationKey(dbSession, organization, "Login");
+
+    OrganizationDto organizationReloaded = dbClient.organizationDao().selectByUuid(dbSession, organization.getUuid()).get();
+    assertThat(organizationReloaded.getKey()).isEqualTo("login");
+  }
+
+  @Test
+  public void fail_to_update_personal_organization_when_new_key_already_exist() {
+    OrganizationDto organization = db.organizations().insert();
+    db.organizations().insert(o -> o.setKey("new_login"));
+    when(organizationValidation.generateKeyFrom("new_login")).thenReturn("new_login");
+
+    expectedException.expect(IllegalStateException.class);
+    expectedException.expectMessage("Can't create organization with key 'new_login' because an organization with this key already exists");
+
+    underTest.updateOrganizationKey(dbSession, organization, "new_login");
+  }
+
+  private void enableCreatePersonalOrg(boolean flag) {
+    settings.setProperty(CorePropertyDefinitions.ORGANIZATIONS_CREATE_PERSONAL_ORG, flag);
+  }
+
+  private void verifyGroupOwners(UserDto user, String organizationKey, String organizationName) {
+    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, organizationKey).get();
+    Optional<GroupDto> groupOpt = dbClient.groupDao().selectByName(dbSession, organization.getUuid(), "Owners");
+    assertThat(groupOpt).isPresent();
+    GroupDto groupDto = groupOpt.get();
+    assertThat(groupDto.getDescription()).isEqualTo("Owners of organization " + organizationName);
+
+    assertThat(dbClient.groupPermissionDao().selectGlobalPermissionsOfGroup(dbSession, groupDto.getOrganizationUuid(), groupDto.getId()))
+      .containsOnly(GlobalPermissions.ALL.toArray(new String[GlobalPermissions.ALL.size()]));
+    List<UserMembershipDto> members = dbClient.groupMembershipDao().selectMembers(
+      dbSession,
+      UserMembershipQuery.builder()
+        .organizationUuid(organization.getUuid())
+        .groupId(groupDto.getId())
+        .membership(UserMembershipQuery.IN).build(),
+      0, Integer.MAX_VALUE);
+    assertThat(members)
+      .extracting(UserMembershipDto::getLogin)
+      .containsOnly(user.getLogin());
+  }
+
+  private void verifyMembersGroup(UserDto user, String organizationKey) {
+    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, organizationKey).get();
+    Optional<GroupDto> groupOpt = dbClient.groupDao().selectByName(dbSession, organization.getUuid(), "Members");
+    assertThat(groupOpt).isPresent();
+    GroupDto groupDto = groupOpt.get();
+    assertThat(groupDto.getDescription()).isEqualTo("All members of the organization");
+
+    assertThat(dbClient.groupPermissionDao().selectGlobalPermissionsOfGroup(dbSession, groupDto.getOrganizationUuid(), groupDto.getId())).isEmpty();
+    List<UserMembershipDto> members = dbClient.groupMembershipDao().selectMembers(
+      dbSession,
+      UserMembershipQuery.builder()
+        .organizationUuid(organization.getUuid())
+        .groupId(groupDto.getId())
+        .membership(UserMembershipQuery.IN).build(),
+      0, Integer.MAX_VALUE);
+    assertThat(members)
+      .extracting(UserMembershipDto::getLogin)
+      .containsOnly(user.getLogin());
+  }
+
+}
index 08e568b0f998c22d7fa86a647a3420a038110519..01af1c6d535c2158c4c85c27586dd8ef70d57f9a 100644 (file)
@@ -47,8 +47,8 @@ import org.sonar.db.user.UserMembershipQuery;
 import org.sonar.server.es.EsTester;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.UnauthorizedException;
-import org.sonar.server.organization.OrganizationCreation;
-import org.sonar.server.organization.OrganizationCreationImpl;
+import org.sonar.server.organization.OrganizationUpdater;
+import org.sonar.server.organization.OrganizationUpdaterImpl;
 import org.sonar.server.organization.OrganizationValidation;
 import org.sonar.server.organization.OrganizationValidationImpl;
 import org.sonar.server.organization.TestOrganizationFlags;
@@ -99,7 +99,7 @@ public class CreateActionTest {
   private OrganizationValidation organizationValidation = new OrganizationValidationImpl();
   private UserIndexer userIndexer = new UserIndexer(dbClient, es.client());
   private UserIndex userIndex = new UserIndex(es.client(), System2.INSTANCE);
-  private OrganizationCreation organizationCreation = new OrganizationCreationImpl(dbClient, system2, UuidFactoryFast.getInstance(), organizationValidation, settings.asConfig(),
+  private OrganizationUpdater organizationUpdater = new OrganizationUpdaterImpl(dbClient, system2, UuidFactoryFast.getInstance(), organizationValidation, settings.asConfig(),
     userIndexer,
     mock(BuiltInQProfileRepository.class), new DefaultGroupCreatorImpl(dbClient));
   private TestOrganizationFlags organizationFlags = TestOrganizationFlags.standalone().setEnabled(true);
@@ -107,7 +107,7 @@ public class CreateActionTest {
   private WsActionTester wsTester = new WsActionTester(
     new CreateAction(settings.asConfig(), userSession, dbClient, new OrganizationsWsSupport(organizationValidation),
       organizationValidation,
-      organizationCreation, organizationFlags));
+      organizationUpdater, organizationFlags));
 
   @Test
   public void verify_define() {
index 26e2835123c0ab8c1c4c93db95592ccd9ec6fb05..f12c7485428c4ee079274c5ce77cd176de42b170 100644 (file)
@@ -42,7 +42,7 @@ import org.sonar.server.authentication.LocalAuthentication.HashMethod;
 import org.sonar.server.es.EsTester;
 import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.organization.DefaultOrganizationProvider;
-import org.sonar.server.organization.OrganizationCreation;
+import org.sonar.server.organization.OrganizationUpdater;
 import org.sonar.server.organization.TestDefaultOrganizationProvider;
 import org.sonar.server.organization.TestOrganizationFlags;
 import org.sonar.server.user.index.UserIndexDefinition;
@@ -82,12 +82,12 @@ public class UserUpdaterCreateTest {
   private ArgumentCaptor<NewUserHandler.Context> newUserHandler = ArgumentCaptor.forClass(NewUserHandler.Context.class);
   private DbSession session = db.getSession();
   private UserIndexer userIndexer = new UserIndexer(dbClient, es.client());
-  private OrganizationCreation organizationCreation = mock(OrganizationCreation.class);
+  private OrganizationUpdater organizationUpdater = mock(OrganizationUpdater.class);
   private DefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(db);
   private TestOrganizationFlags organizationFlags = TestOrganizationFlags.standalone();
   private MapSettings settings = new MapSettings();
   private LocalAuthentication localAuthentication = new LocalAuthentication(db.getDbClient());
-  private UserUpdater underTest = new UserUpdater(newUserNotifier, dbClient, userIndexer, organizationFlags, defaultOrganizationProvider, organizationCreation,
+  private UserUpdater underTest = new UserUpdater(newUserNotifier, dbClient, userIndexer, organizationFlags, defaultOrganizationProvider, organizationUpdater,
     new DefaultGroupFinder(dbClient), settings.asConfig(), localAuthentication);
 
   @Test
@@ -598,7 +598,7 @@ public class UserUpdaterCreateTest {
       .build(), u -> {
       });
 
-    verify(organizationCreation).createForUser(any(DbSession.class), eq(dto));
+    verify(organizationUpdater).createForUser(any(DbSession.class), eq(dto));
   }
 
   @Test
index 7a0213645213a8ca67752348f990ef2e1b6fee57..265346936719de9e3f59bc3ddd2e6e44228d6593 100644 (file)
@@ -36,7 +36,7 @@ import org.sonar.server.authentication.LocalAuthentication;
 import org.sonar.server.authentication.LocalAuthentication.HashMethod;
 import org.sonar.server.es.EsTester;
 import org.sonar.server.organization.DefaultOrganizationProvider;
-import org.sonar.server.organization.OrganizationCreation;
+import org.sonar.server.organization.OrganizationUpdater;
 import org.sonar.server.organization.TestDefaultOrganizationProvider;
 import org.sonar.server.organization.TestOrganizationFlags;
 import org.sonar.server.user.index.UserIndexer;
@@ -65,12 +65,12 @@ public class UserUpdaterReactivateTest {
   private NewUserNotifier newUserNotifier = mock(NewUserNotifier.class);
   private DbSession session = db.getSession();
   private UserIndexer userIndexer = new UserIndexer(dbClient, es.client());
-  private OrganizationCreation organizationCreation = mock(OrganizationCreation.class);
+  private OrganizationUpdater organizationUpdater = mock(OrganizationUpdater.class);
   private DefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(db);
   private TestOrganizationFlags organizationFlags = TestOrganizationFlags.standalone();
   private MapSettings settings = new MapSettings();
   private LocalAuthentication localAuthentication = new LocalAuthentication(db.getDbClient());
-  private UserUpdater underTest = new UserUpdater(newUserNotifier, dbClient, userIndexer, organizationFlags, defaultOrganizationProvider, organizationCreation,
+  private UserUpdater underTest = new UserUpdater(newUserNotifier, dbClient, userIndexer, organizationFlags, defaultOrganizationProvider, organizationUpdater,
     new DefaultGroupFinder(dbClient), settings.asConfig(), localAuthentication);
 
   @Test
index 4545218baa74c5b3559590707e7de72fabcd1eb7..26327f14d6059f9c547b74ae9d3443d077ec4a56 100644 (file)
@@ -41,7 +41,7 @@ import org.sonar.server.authentication.LocalAuthentication;
 import org.sonar.server.es.EsTester;
 import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.organization.DefaultOrganizationProvider;
-import org.sonar.server.organization.OrganizationCreation;
+import org.sonar.server.organization.OrganizationUpdater;
 import org.sonar.server.organization.TestDefaultOrganizationProvider;
 import org.sonar.server.organization.TestOrganizationFlags;
 import org.sonar.server.user.index.UserIndexDefinition;
@@ -77,12 +77,12 @@ public class UserUpdaterUpdateTest {
   private NewUserNotifier newUserNotifier = mock(NewUserNotifier.class);
   private DbSession session = db.getSession();
   private UserIndexer userIndexer = new UserIndexer(dbClient, es.client());
-  private OrganizationCreation organizationCreation = mock(OrganizationCreation.class);
+  private OrganizationUpdater organizationUpdater = mock(OrganizationUpdater.class);
   private DefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(db);
   private TestOrganizationFlags organizationFlags = TestOrganizationFlags.standalone();
   private MapSettings settings = new MapSettings();
   private LocalAuthentication localAuthentication = new LocalAuthentication(db.getDbClient());
-  private UserUpdater underTest = new UserUpdater(newUserNotifier, dbClient, userIndexer, organizationFlags, defaultOrganizationProvider, organizationCreation,
+  private UserUpdater underTest = new UserUpdater(newUserNotifier, dbClient, userIndexer, organizationFlags, defaultOrganizationProvider, organizationUpdater,
     new DefaultGroupFinder(dbClient), settings.asConfig(), localAuthentication);
 
   @Test
index 99d613f90a8ed4332fc0c2fc0c4b5696736a5893..c8f99ed0895845f41d1820a0930c9ac801c93800 100644 (file)
@@ -30,7 +30,7 @@ import org.sonar.server.es.EsTester;
 import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.NotFoundException;
-import org.sonar.server.organization.OrganizationCreation;
+import org.sonar.server.organization.OrganizationUpdater;
 import org.sonar.server.organization.TestDefaultOrganizationProvider;
 import org.sonar.server.organization.TestOrganizationFlags;
 import org.sonar.server.tester.UserSessionRule;
@@ -63,7 +63,7 @@ public class ChangePasswordActionTest {
   private UserUpdater userUpdater = new UserUpdater(mock(NewUserNotifier.class), db.getDbClient(), new UserIndexer(db.getDbClient(), es.client()),
     organizationFlags,
     TestDefaultOrganizationProvider.from(db),
-    mock(OrganizationCreation.class),
+    mock(OrganizationUpdater.class),
     new DefaultGroupFinder(db.getDbClient()),
     new MapSettings().asConfig(),
     localAuthentication);
index 5c117ca18721cc0b6b2aa27028d14afdd034f0e9..6ca46d0fa5db3b105fa664584f0b3e3d68b3bebb 100644 (file)
@@ -38,7 +38,7 @@ import org.sonar.server.authentication.LocalAuthentication;
 import org.sonar.server.es.EsTester;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.organization.DefaultOrganizationProvider;
-import org.sonar.server.organization.OrganizationCreation;
+import org.sonar.server.organization.OrganizationUpdater;
 import org.sonar.server.organization.TestDefaultOrganizationProvider;
 import org.sonar.server.organization.TestOrganizationFlags;
 import org.sonar.server.tester.UserSessionRule;
@@ -89,12 +89,12 @@ public class CreateActionTest {
   private GroupDto defaultGroupInDefaultOrg;
   private DefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(db);
   private TestOrganizationFlags organizationFlags = TestOrganizationFlags.standalone();
-  private OrganizationCreation organizationCreation = mock(OrganizationCreation.class);
+  private OrganizationUpdater organizationUpdater = mock(OrganizationUpdater.class);
   private LocalAuthentication localAuthentication = new LocalAuthentication(db.getDbClient());
   private WsActionTester tester = new WsActionTester(new CreateAction(
     db.getDbClient(),
     new UserUpdater(mock(NewUserNotifier.class), db.getDbClient(), userIndexer, organizationFlags, defaultOrganizationProvider,
-      organizationCreation, new DefaultGroupFinder(db.getDbClient()), settings.asConfig(), localAuthentication),
+      organizationUpdater, new DefaultGroupFinder(db.getDbClient()), settings.asConfig(), localAuthentication),
     userSessionRule));
 
   @Before
@@ -389,7 +389,7 @@ public class CreateActionTest {
     assertThat(dbUser).isPresent();
 
     ArgumentCaptor<UserDto> userCaptor = ArgumentCaptor.forClass(UserDto.class);
-    verify(organizationCreation).createForUser(any(DbSession.class), userCaptor.capture());
+    verify(organizationUpdater).createForUser(any(DbSession.class), userCaptor.capture());
     assertThat(userCaptor.getValue().getId()).isEqualTo(dbUser.get().getId());
   }
 
index 46f0713e656b08ed873eabda623d24aefb346d0e..6b9960605f49523ee5a5a928b94c269da7e22820 100644 (file)
@@ -35,7 +35,7 @@ import org.sonar.server.es.EsTester;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.organization.DefaultOrganizationProvider;
-import org.sonar.server.organization.OrganizationCreation;
+import org.sonar.server.organization.OrganizationUpdater;
 import org.sonar.server.organization.TestDefaultOrganizationProvider;
 import org.sonar.server.organization.TestOrganizationFlags;
 import org.sonar.server.tester.UserSessionRule;
@@ -53,7 +53,7 @@ import static org.sonar.db.user.UserTesting.newUserDto;
 
 public class UpdateActionTest {
 
-  private static final OrganizationCreation ORGANIZATION_CREATION_NOT_USED_FOR_UPDATE = null;
+  private static final OrganizationUpdater ORGANIZATION_CREATION_NOT_USED_FOR_UPDATE = null;
   private MapSettings settings = new MapSettings();
   private System2 system2 = new System2();
 
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/UpdateLogin.tsx b/server/sonar-web/src/main/js/apps/sessions/components/UpdateLogin.tsx
new file mode 100644 (file)
index 0000000..82f81c3
--- /dev/null
@@ -0,0 +1,156 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.
+ */
+import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { getIdentityProviders } from '../../../api/users';
+import * as theme from '../../../app/theme';
+import { IdentityProvider } from '../../../app/types';
+import { getTextColor } from '../../../helpers/colors';
+import { translate } from '../../../helpers/l10n';
+import { getBaseUrl } from '../../../helpers/urls';
+
+interface Props {
+  location: {
+    query: {
+      login: string;
+      providerKey: string;
+      providerName: string;
+      oldLogin: string;
+      oldOrganizationKey: string;
+    };
+  };
+}
+
+interface State {
+  identityProviders: IdentityProvider[];
+  loading: boolean;
+}
+
+export default class UpdateLogin extends React.PureComponent<Props, State> {
+  mounted = false;
+  state: State = { identityProviders: [], loading: true };
+
+  componentDidMount() {
+    this.mounted = true;
+    this.fetchIdentityProviders();
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  fetchIdentityProviders = () => {
+    this.setState({ loading: true });
+    getIdentityProviders().then(
+      ({ identityProviders }) => {
+        if (this.mounted) {
+          this.setState({ identityProviders, loading: false });
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({ loading: false });
+        }
+      }
+    );
+  };
+
+  renderIdentityProvier = (provider: string, login: string) => {
+    const identityProvider = this.state.identityProviders.find(p => p.key === provider);
+
+    return identityProvider ? (
+      <div
+        className="identity-provider"
+        style={{
+          backgroundColor: identityProvider.backgroundColor,
+          color: getTextColor(identityProvider.backgroundColor, theme.secondFontColor)
+        }}>
+        <img
+          alt={identityProvider.name}
+          className="little-spacer-right"
+          src={getBaseUrl() + identityProvider.iconPath}
+          width="14"
+          height="14"
+        />
+        {login}
+      </div>
+    ) : (
+      <div>
+        {provider !== 'sonarqube' && provider} {login}
+      </div>
+    );
+  };
+
+  render() {
+    const { query } = this.props.location;
+
+    return (
+      <div id="bd" className="page-wrapper-simple">
+        <div id="nonav" className="page-simple">
+          <div className="big-spacer-bottom js-provider-name">
+            <p className="little-spacer-bottom">
+              <FormattedMessage
+                  defaultMessage={translate('sessions.update_login.1')}
+                  id="sessions.update_login.1"
+                  values={{ providerName: <strong>{query.providerName}</strong> }}
+                />
+            </p>
+          </div>
+
+          <div className="big-spacer-bottom js-new-account">
+            <p className="little-spacer-bottom">{translate('sessions.update_login.2')}</p>
+            {this.renderIdentityProvier(query.providerKey, query.login)}
+          </div>
+
+          <div className="alert alert-warning">
+            {translate('sessions.update_login.3')}
+            <ul className="list-styled">
+              <li className="spacer-top js-old-organization-key">
+                <FormattedMessage
+                    defaultMessage={translate('sessions.update_login.4')}
+                    id="sessions.update_login.4"
+                    values={{ organizationKey: <strong>{query.oldOrganizationKey}</strong> }}
+                  />
+              </li>
+              <li className="spacer-top js-old-login">
+                <FormattedMessage
+                    defaultMessage={translate('sessions.update_login.5')}
+                    id="sessions.update_login.5"
+                    values={{ login: <strong>{query.oldLogin}</strong> }}
+                  />
+              </li>
+            </ul>
+          </div>
+
+          <div className="big-spacer-top text-right">
+            <a
+              className="button js-continue"
+              href={`${getBaseUrl()}/sessions/init/${query.providerKey}?allowUpdateLogin=true`}>
+              {translate('continue')}
+            </a>
+            <a className="big-spacer-left js-cancel" href={getBaseUrl() + '/'}>
+              {translate('cancel')}
+            </a>
+          </div>
+        </div>
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/UpdateLogin-test.tsx b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/UpdateLogin-test.tsx
new file mode 100644 (file)
index 0000000..24ced29
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import UpdateLogin from '../UpdateLogin';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
+
+jest.mock('../../../../api/users', () => ({
+  getIdentityProviders: () =>
+    Promise.resolve({
+      identityProviders: [
+        {
+          key: 'bitbucket',
+          name: 'Bitbucket',
+          iconPath: '/static/authbitbucket/bitbucket.svg',
+          backgroundColor: '#205081'
+        },
+        {
+          key: 'github',
+          name: 'GitHub',
+          iconPath: '/static/authgithub/github.svg',
+          backgroundColor: '#444444'
+        }
+      ]
+    })
+}));
+
+it('render', async () => {
+  const query = {
+    login: 'foo',
+    providerKey: 'github',
+    providerName: 'GitHub',
+    oldLogin: 'old_foo',
+    oldOrganizationKey: 'old_foo@github'
+  };
+  const wrapper = shallow(<UpdateLogin location={{ query }} />);
+  (wrapper.instance() as UpdateLogin).mounted = true;
+  (wrapper.instance() as UpdateLogin).fetchIdentityProviders();
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/UpdateLogin-test.tsx.snap b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/UpdateLogin-test.tsx.snap
new file mode 100644 (file)
index 0000000..e6bdf41
--- /dev/null
@@ -0,0 +1,115 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`render 1`] = `
+<div
+  className="page-wrapper-simple"
+  id="bd"
+>
+  <div
+    className="page-simple"
+    id="nonav"
+  >
+    <div
+      className="big-spacer-bottom js-provider-name"
+    >
+      <p
+        className="little-spacer-bottom"
+      >
+        <FormattedMessage
+          defaultMessage="sessions.update_login.1"
+          id="sessions.update_login.1"
+          values={
+            Object {
+              "providerName": <strong>
+                GitHub
+              </strong>,
+            }
+          }
+        />
+      </p>
+    </div>
+    <div
+      className="big-spacer-bottom js-new-account"
+    >
+      <p
+        className="little-spacer-bottom"
+      >
+        sessions.update_login.2
+      </p>
+      <div
+        className="identity-provider"
+        style={
+          Object {
+            "backgroundColor": "#444444",
+            "color": "#fff",
+          }
+        }
+      >
+        <img
+          alt="GitHub"
+          className="little-spacer-right"
+          height="14"
+          src="/static/authgithub/github.svg"
+          width="14"
+        />
+        foo
+      </div>
+    </div>
+    <div
+      className="alert alert-warning"
+    >
+      sessions.update_login.3
+      <ul
+        className="list-styled"
+      >
+        <li
+          className="spacer-top js-old-organization-key"
+        >
+          <FormattedMessage
+            defaultMessage="sessions.update_login.4"
+            id="sessions.update_login.4"
+            values={
+              Object {
+                "organizationKey": <strong>
+                  old_foo@github
+                </strong>,
+              }
+            }
+          />
+        </li>
+        <li
+          className="spacer-top js-old-login"
+        >
+          <FormattedMessage
+            defaultMessage="sessions.update_login.5"
+            id="sessions.update_login.5"
+            values={
+              Object {
+                "login": <strong>
+                  old_foo
+                </strong>,
+              }
+            }
+          />
+        </li>
+      </ul>
+    </div>
+    <div
+      className="big-spacer-top text-right"
+    >
+      <a
+        className="button js-continue"
+        href="/sessions/init/github?allowUpdateLogin=true"
+      >
+        continue
+      </a>
+      <a
+        className="big-spacer-left js-cancel"
+        href="/"
+      >
+        cancel
+      </a>
+    </div>
+  </div>
+</div>
+`;
index 28945131363349a066676cc3b6cd5555d8e5d12e..b3089c314ff346642909c76030a5334b29dcbcf1 100644 (file)
@@ -35,6 +35,10 @@ const routes = [
   {
     path: 'email_already_exists',
     component: lazyLoad(() => import('./components/EmailAlreadyExists'))
+  },
+  {
+    path: 'update_login',
+    component: lazyLoad(() => import('./components/UpdateLogin'))
   }
 ];
 
index b29e30924bc153394916b0063f0e325193154723..2f5a9e34af826427deffd0039a2cc5ad6c7f6da4 100644 (file)
@@ -540,7 +540,11 @@ sessions.email_already_exists.3=This means the following:
 sessions.email_already_exists.4=Your email address will be erased from the first account.
 sessions.email_already_exists.5=You will no longer receive email notifications from this account.
 sessions.email_already_exists.6=Issues won't be automatically assigned to this account anymore.
-
+sessions.update_login.1=We noticed that you've renamed your username on {providerName}.
+sessions.update_login.2=By clicking on "Continue" we will update the key of your personal organization and rename your login to :
+sessions.update_login.3=This means that you need to update :
+sessions.update_login.4=Build scripts that might be using your old personal organization key ({organizationKey}).
+sessions.update_login.5=Any Web Service call that might be referencing your old login ({login}).
 
 #------------------------------------------------------------------------------
 #
index 6e6427e72fd2c2d087621394cb9ebaadc4d38441..4545bc08ecd99890daff4529d238cb3400a272d3 100644 (file)
@@ -304,20 +304,22 @@ public class BaseIdentityProviderTest {
   @Test
   public void update_login() {
     enablePlugin();
-    String oldLogin = "login_to_update@base";
-    setUserCreatedByAuthPlugin(oldLogin, "login_to_update_id", "old_provider_login", USER_NAME, USER_EMAIL);
+    String oldLogin = tester.users().generateLogin();
+    String providerId = tester.users().generateProviderId();
+    setUserCreatedByAuthPlugin(oldLogin, providerId, tester.users().generateLogin(), USER_NAME, USER_EMAIL);
     assertThat(tester.users().getByLogin(USER_LOGIN)).isNotPresent();
     authenticateWithFakeAuthProvider();
 
     // Login is updated
-    String newLogin = "new_login@base";
-    setUserCreatedByAuthPlugin(newLogin, "login_to_update_id", "new_provider_login", USER_NAME, USER_EMAIL);
+    String newLogin = tester.users().generateLogin();
+    String newProviderLogin = tester.users().generateLogin();
+    setUserCreatedByAuthPlugin(newLogin, providerId, newProviderLogin, USER_NAME, USER_EMAIL);
     authenticateWithFakeAuthProvider();
 
     assertThat(tester.users().getByLogin(oldLogin)).isNotPresent();
     assertThat(tester.users().service().search(new SearchRequest().setQ(newLogin)).getUsersList())
       .extracting(User::getLogin, User::getName, User::getEmail, User::getExternalProvider, User::getExternalIdentity, User::getLocal)
-      .containsExactlyInAnyOrder(tuple(newLogin, USER_NAME, USER_EMAIL, FAKE_PROVIDER_KEY, "new_provider_login", false));
+      .containsExactlyInAnyOrder(tuple(newLogin, USER_NAME, USER_EMAIL, FAKE_PROVIDER_KEY, newProviderLogin, false));
     // Check that searching for old login return nothing
     assertThat(tester.users().service().search(new SearchRequest().setQ(oldLogin)).getPaging().getTotal()).isZero();
   }
@@ -325,8 +327,8 @@ public class BaseIdentityProviderTest {
   @Test
   public void authenticate_with_external_id_null() {
     enablePlugin();
-    String login = "no_external_id_login";
-    setUserCreatedByAuthPlugin(login, null, "no_external_id_provider_login", USER_NAME, USER_EMAIL);
+    String login = tester.users().generateLogin();
+    setUserCreatedByAuthPlugin(login, null, login, USER_NAME, USER_EMAIL);
 
     // First authentication
     authenticateWithFakeAuthProvider();
@@ -341,12 +343,12 @@ public class BaseIdentityProviderTest {
   @Test
   public void update_external_id() {
     enablePlugin();
-    String login = "update_external_id_login";
-    setUserCreatedByAuthPlugin(login, "old_external_id", login, USER_NAME, USER_EMAIL);
+    String login = tester.users().generateLogin();
+    setUserCreatedByAuthPlugin(login, tester.users().generateProviderId(), login, USER_NAME, USER_EMAIL);
     authenticateWithFakeAuthProvider();
     assertThat(tester.users().getByLogin(login)).isPresent();
 
-    setUserCreatedByAuthPlugin(login, "new_external_id", login, USER_NAME, USER_EMAIL);
+    setUserCreatedByAuthPlugin(login, tester.users().generateProviderId(), login, USER_NAME, USER_EMAIL);
     authenticateWithFakeAuthProvider();
     assertThat(tester.users().getByLogin(login)).isPresent();
   }
@@ -356,7 +358,8 @@ public class BaseIdentityProviderTest {
   }
 
   private void setUserCreatedByAuthPlugin(String login, @Nullable String providerId, String providerLogin, String name, String email) {
-    tester.settings().setGlobalSettings("sonar.auth.fake-base-id-provider.user", login + "," + (providerId == null ? "" : providerId) + "," + providerLogin + "," + name + "," + email);
+    tester.settings().setGlobalSettings("sonar.auth.fake-base-id-provider.user",
+      login + "," + (providerId == null ? "" : providerId) + "," + providerLogin + "," + name + "," + email);
   }
 
   private void setGroupsReturnedByAuthPlugin(String... groups) {
index 90933ed35c7cff1e58e0873f27664beba3ac6469..b7e194b37859694ee5db6854b12f67f24fbfeb9b 100644 (file)
@@ -234,13 +234,16 @@ public class OAuth2IdentityProviderTest {
   public void update_login() {
     simulateRedirectionToCallback();
     enablePlugin();
+    String providerId = tester.users().generateProviderId();
 
-    String oldLogin = "login_to_update@oauth2";
-    tester.settings().setGlobalSettings("sonar.auth.fake-oauth2-id-provider.user", oldLogin + "," + "login_to_update_id" + "," + "old_provider_login" + "," + USER_NAME + "," + USER_EMAIL);
+    String oldLogin = tester.users().generateLogin();
+    tester.settings().setGlobalSettings("sonar.auth.fake-oauth2-id-provider.user",
+      oldLogin + "," + providerId + "," + tester.users().generateLogin() + "," + USER_NAME + "," + USER_EMAIL);
     authenticateWithFakeAuthProvider();
 
-    String newLogin = "new_logine@oauth2";
-    tester.settings().setGlobalSettings("sonar.auth.fake-oauth2-id-provider.user", newLogin + "," + "login_to_update_id" + "," + "new_provider_login" + "," + USER_NAME + "," + USER_EMAIL);
+    String newLogin = tester.users().generateLogin();
+    tester.settings().setGlobalSettings("sonar.auth.fake-oauth2-id-provider.user",
+      newLogin + "," + providerId + "," + tester.users().generateLogin() + "," + USER_NAME + "," + USER_EMAIL);
     authenticateWithFakeAuthProvider();
 
     verifyUser(newLogin, USER_NAME, USER_EMAIL);
@@ -272,7 +275,8 @@ public class OAuth2IdentityProviderTest {
   private void enablePlugin() {
     tester.settings().setGlobalSettings("sonar.auth.fake-oauth2-id-provider.enabled", "true");
     tester.settings().setGlobalSettings("sonar.auth.fake-oauth2-id-provider.url", fakeServerAuthProviderUrl);
-    tester.settings().setGlobalSettings("sonar.auth.fake-oauth2-id-provider.user", USER_LOGIN + "," + USER_PROVIDER_ID + "," + USER_PROVIDER_LOGIN + "," + USER_NAME + "," + USER_EMAIL);
+    tester.settings().setGlobalSettings("sonar.auth.fake-oauth2-id-provider.user",
+      USER_LOGIN + "," + USER_PROVIDER_ID + "," + USER_PROVIDER_LOGIN + "," + USER_NAME + "," + USER_EMAIL);
   }
 
   private void assertThatUserDoesNotExist(String login) {
diff --git a/tests/src/test/java/org/sonarqube/tests/user/OrganizationBaseIdentityProviderTest.java b/tests/src/test/java/org/sonarqube/tests/user/OrganizationBaseIdentityProviderTest.java
new file mode 100644 (file)
index 0000000..d8eafb3
--- /dev/null
@@ -0,0 +1,119 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.sonarqube.tests.user;
+
+import com.google.common.base.Joiner;
+import com.sonar.orchestrator.Orchestrator;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.qa.util.Tester;
+import org.sonarqube.ws.UserGroups.Group;
+import org.sonarqube.ws.Users.CreateWsResponse.User;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.organizations.AddMemberRequest;
+
+public class OrganizationBaseIdentityProviderTest {
+
+  @ClassRule
+  public static Orchestrator orchestrator = SonarCloudUserSuite.ORCHESTRATOR;
+
+  @Rule
+  public Tester tester = new Tester(orchestrator);
+
+  @Before
+  public void setUp() {
+    // enable the fake authentication plugin
+    tester.settings().setGlobalSettings("sonar.auth.fake-base-id-provider.enabled", "true");
+  }
+
+  @After
+  public void tearDown() {
+    tester.settings().resetSettings("sonar.auth.fake-base-id-provider.enabled",
+      "sonar.auth.fake-base-id-provider.user",
+      "sonar.auth.fake-base-id-provider.throwUnauthorizedMessage",
+      "sonar.auth.fake-base-id-provider.enabledGroupsSync",
+      "sonar.auth.fake-base-id-provider.groups",
+      "sonar.auth.fake-base-id-provider.allowsUsersToSignUp");
+  }
+
+  @Test
+  public void synchronize_groups_for_new_user() {
+    Group group = tester.groups().generate();
+    String login = tester.users().generateLogin();
+    enableUserCreationByAuthPlugin(login);
+    setGroupsReturnedByAuthPlugin(group.getName());
+
+    authenticateWithFakeAuthProvider();
+
+    // No default group membership
+    tester.groups().assertThatUserIsOnlyMemberOf(null, login, group.getName());
+  }
+
+  @Test
+  public void synchronize_groups_for_existing_user() {
+    Group group = tester.groups().generate();
+    User user = tester.users().generate();
+    enableUserCreationByAuthPlugin(user.getLogin());
+    setGroupsReturnedByAuthPlugin(group.getName());
+
+    authenticateWithFakeAuthProvider();
+
+    // No default group membership
+    tester.groups().assertThatUserIsOnlyMemberOf(null, user.getLogin(), group.getName());
+  }
+
+  @Test
+  public void remove_default_group() {
+    Group group = tester.groups().generate();
+    User user = tester.users().generate();
+    // Add user as member of default organization
+    tester.wsClient().organizations().addMember(new AddMemberRequest().setOrganization("default-organization").setLogin(user.getLogin()));
+    tester.groups().assertThatUserIsMemberOf(null, user.getLogin(), "Members");
+    enableUserCreationByAuthPlugin(user.getLogin());
+    // No group is returned by the plugin
+    setGroupsReturnedByAuthPlugin();
+
+    authenticateWithFakeAuthProvider();
+
+    // No default group membership
+    tester.groups().assertThatUserIsOnlyMemberOf(null, user.getLogin());
+  }
+
+  private void enableUserCreationByAuthPlugin(String login) {
+    tester.settings().setGlobalSettings("sonar.auth.fake-base-id-provider.user", login + "," + login + ",fake-" + login + ",John,john@email.com");
+  }
+
+  private void setGroupsReturnedByAuthPlugin(String... groups) {
+    tester.settings().setGlobalSettings("sonar.auth.fake-base-id-provider.enabledGroupsSync", "true");
+    if (groups.length > 0) {
+      tester.settings().setGlobalSettings("sonar.auth.fake-base-id-provider.groups", Joiner.on(",").join(groups));
+    }
+  }
+
+  private void authenticateWithFakeAuthProvider() {
+    tester.wsClient().wsConnector().call(
+      new GetRequest("/sessions/init/fake-base-id-provider"))
+      .failIfNotSuccessful();
+  }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/user/OrganizationIdentityProviderTest.java b/tests/src/test/java/org/sonarqube/tests/user/OrganizationIdentityProviderTest.java
deleted file mode 100644 (file)
index a05dbd0..0000000
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info 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.sonarqube.tests.user;
-
-import com.google.common.base.Joiner;
-import com.sonar.orchestrator.Orchestrator;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.ClassRule;
-import org.junit.Rule;
-import org.junit.Test;
-import org.sonarqube.qa.util.Tester;
-import org.sonarqube.ws.UserGroups.Group;
-import org.sonarqube.ws.Users.CreateWsResponse.User;
-import org.sonarqube.ws.client.GetRequest;
-import org.sonarqube.ws.client.organizations.AddMemberRequest;
-
-public class OrganizationIdentityProviderTest {
-
-  @ClassRule
-  public static Orchestrator orchestrator = SonarCloudUserSuite.ORCHESTRATOR;
-
-  @Rule
-  public Tester tester = new Tester(orchestrator);
-
-  @Before
-  public void setUp() {
-    // enable the fake authentication plugin
-    tester.settings().setGlobalSettings("sonar.auth.fake-base-id-provider.enabled", "true");
-  }
-
-  @After
-  public void tearDown() {
-    tester.settings().resetSettings("sonar.auth.fake-base-id-provider.enabled",
-      "sonar.auth.fake-base-id-provider.user",
-      "sonar.auth.fake-base-id-provider.throwUnauthorizedMessage",
-      "sonar.auth.fake-base-id-provider.enabledGroupsSync",
-      "sonar.auth.fake-base-id-provider.groups",
-      "sonar.auth.fake-base-id-provider.allowsUsersToSignUp");
-  }
-
-  @Test
-  public void default_group_is_not_added_for_new_user() {
-    Group group = tester.groups().generate();
-    enableUserCreationByAuthPlugin("aLogin");
-    setGroupsReturnedByAuthPlugin(group.getName());
-
-    authenticateWithFakeAuthProvider();
-
-    // No default group membership
-    tester.groups().assertThatUserIsOnlyMemberOf(null, "aLogin", group.getName());
-  }
-
-  @Test
-  public void default_group_is_not_sync_for_existing_user() {
-    Group group = tester.groups().generate();
-    User user = tester.users().generate();
-    enableUserCreationByAuthPlugin(user.getLogin());
-    setGroupsReturnedByAuthPlugin(group.getName());
-
-    authenticateWithFakeAuthProvider();
-
-    // No default group membership
-    tester.groups().assertThatUserIsOnlyMemberOf(null, user.getLogin(), group.getName());
-  }
-
-  @Test
-  public void remove_default_group() {
-    Group group = tester.groups().generate();
-    User user = tester.users().generate();
-    // Add user as member of default organization
-    tester.wsClient().organizations().addMember(new AddMemberRequest().setOrganization("default-organization").setLogin(user.getLogin()));
-    tester.groups().assertThatUserIsMemberOf(null, user.getLogin(), "Members");
-    enableUserCreationByAuthPlugin(user.getLogin());
-    // No group is returned by the plugin
-    setGroupsReturnedByAuthPlugin();
-
-    authenticateWithFakeAuthProvider();
-
-    // No default group membership
-    tester.groups().assertThatUserIsOnlyMemberOf(null, user.getLogin());
-  }
-
-  private void enableUserCreationByAuthPlugin(String login) {
-    tester.settings().setGlobalSettings("sonar.auth.fake-base-id-provider.user", login +"," + login + ",fake-" + login + ",John,john@email.com");
-  }
-
-  private void setGroupsReturnedByAuthPlugin(String... groups) {
-    tester.settings().setGlobalSettings("sonar.auth.fake-base-id-provider.enabledGroupsSync", "true");
-    if (groups.length > 0) {
-      tester.settings().setGlobalSettings("sonar.auth.fake-base-id-provider.groups", Joiner.on(",").join(groups));
-    }
-  }
-
-  private void authenticateWithFakeAuthProvider() {
-    tester.wsClient().wsConnector().call(
-      new GetRequest("/sessions/init/fake-base-id-provider"))
-      .failIfNotSuccessful();
-  }
-
-}
diff --git a/tests/src/test/java/org/sonarqube/tests/user/SonarCloudOAuth2IdentityProviderTest.java b/tests/src/test/java/org/sonarqube/tests/user/SonarCloudOAuth2IdentityProviderTest.java
new file mode 100644 (file)
index 0000000..d7e77ca
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.sonarqube.tests.user;
+
+import com.sonar.orchestrator.Orchestrator;
+import java.net.HttpURLConnection;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.qa.util.Tester;
+import org.sonarqube.ws.Organizations;
+import org.sonarqube.ws.Users;
+import org.sonarqube.ws.client.organizations.SearchRequest;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class SonarCloudOAuth2IdentityProviderTest {
+
+  @ClassRule
+  public static Orchestrator orchestrator = SonarCloudUserSuite.ORCHESTRATOR;
+
+  private static String FAKE_PROVIDER_KEY = "fake-oauth2-id-provider";
+
+  @Rule
+  public Tester tester = new Tester(orchestrator);
+
+  private MockWebServer fakeServerAuthProvider;
+
+  @Before
+  public void setUp() throws Exception {
+    fakeServerAuthProvider = new MockWebServer();
+    fakeServerAuthProvider.start();
+    tester.settings().setGlobalSettings("sonar.auth.fake-oauth2-id-provider.enabled", "true");
+    tester.settings().setGlobalSettings("sonar.auth.fake-oauth2-id-provider.url", fakeServerAuthProvider.url("").url().toString());
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    tester.settings().resetSettings(
+      "sonar.auth.fake-oauth2-id-provider.enabled",
+      "sonar.auth.fake-oauth2-id-provider.url",
+      "sonar.auth.fake-oauth2-id-provider.user");
+    fakeServerAuthProvider.shutdown();
+  }
+
+  @Test
+  public void user_is_redirect_to_warning_page_when_updating_login_and_personal_organization_key() {
+    String oldLogin = tester.users().generateLogin();
+    String newLogin = tester.users().generateLogin();
+    String providerId = tester.users().generateProviderId();
+    tester.settings().setGlobalSettings("sonar.organizations.createPersonalOrg", "true");
+
+    // First authentication to create the user with its personal organization
+    tester.settings().setGlobalSettings("sonar.auth.fake-oauth2-id-provider.user", oldLogin + "," + providerId + ",fake-" + oldLogin + ",John,john@email.com");
+    simulateRedirectionToCallback();
+    tester.openBrowser().openLogin().useOAuth2().shouldBeLoggedIn();
+    assertThat(tester.organizations().service().search(new SearchRequest()).getOrganizationsList())
+      .extracting(Organizations.Organization::getKey)
+      .contains(oldLogin);
+
+    // Second authentication, login is updated
+    tester.settings().setGlobalSettings("sonar.auth.fake-oauth2-id-provider.user", newLogin + "," + providerId + ",fake-" + newLogin + ",John,john@email.com");
+    simulateRedirectionToCallback();
+    tester.openBrowser()
+      .logIn()
+      .useOAuth2()
+      .asUpdateLoginPage()
+      .shouldHaveProviderName("Fake oauth2 identity provider")
+      .shouldHaveNewAccount("fake-" + newLogin)
+      .shouldHaveOldLogin(oldLogin)
+      .shouldHaveOldOrganizationKey(oldLogin)
+      .clickContinue();
+
+    assertThat(tester.users().service().search(new org.sonarqube.ws.client.users.SearchRequest()).getUsersList())
+      .extracting(Users.SearchWsResponse.User::getLogin)
+      .contains(newLogin)
+      .doesNotContainSequence(oldLogin);
+    assertThat(tester.organizations().service().search(new SearchRequest()).getOrganizationsList())
+      .extracting(Organizations.Organization::getKey)
+      .contains(newLogin)
+      .doesNotContain(oldLogin);
+  }
+
+  private void simulateRedirectionToCallback() {
+    fakeServerAuthProvider.enqueue(new MockResponse()
+      .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
+      .addHeader("Location: " + orchestrator.getServer().getUrl() + "/oauth2/callback/" + FAKE_PROVIDER_KEY)
+      .setBody("Redirect to SonarQube"));
+    fakeServerAuthProvider.enqueue(new MockResponse()
+      .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
+      .addHeader("Location: " + orchestrator.getServer().getUrl() + "/oauth2/callback/" + FAKE_PROVIDER_KEY)
+      .setBody("Redirect to SonarQube"));
+  }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/user/SonarCloudUpdateLoginDuringAuthenticationTest.java b/tests/src/test/java/org/sonarqube/tests/user/SonarCloudUpdateLoginDuringAuthenticationTest.java
new file mode 100644 (file)
index 0000000..392c75b
--- /dev/null
@@ -0,0 +1,163 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.sonarqube.tests.user;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import java.util.List;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.qa.util.Tester;
+import org.sonarqube.ws.Issues.Issue;
+import org.sonarqube.ws.Organizations.Organization;
+import org.sonarqube.ws.Projects;
+import org.sonarqube.ws.Qualityprofiles;
+import org.sonarqube.ws.Settings;
+import org.sonarqube.ws.Users;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.organizations.AddMemberRequest;
+import org.sonarqube.ws.client.organizations.SearchRequest;
+import org.sonarqube.ws.client.settings.SetRequest;
+import org.sonarqube.ws.client.settings.ValuesRequest;
+
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.projectDir;
+
+public class SonarCloudUpdateLoginDuringAuthenticationTest {
+
+  @ClassRule
+  public static Orchestrator orchestrator = SonarCloudUserSuite.ORCHESTRATOR;
+
+  @Rule
+  public Tester tester = new Tester(orchestrator);
+
+  @Before
+  public void setUp() {
+    // enable the fake authentication plugin
+    tester.settings().setGlobalSettings("sonar.auth.fake-base-id-provider.enabled", "true");
+  }
+
+  @After
+  public void tearDown() {
+    tester.settings().resetSettings("sonar.auth.fake-base-id-provider.enabled", "sonar.auth.fake-base-id-provider.user");
+  }
+
+  @Test
+  public void update_login_and_personal_organization_when_authenticating_existing_user() {
+    tester.settings().setGlobalSettings("sonar.organizations.createPersonalOrg", "true");
+    String oldLogin = tester.users().generateLogin();
+    String providerId = tester.users().generateProviderId();
+    authenticate(oldLogin, providerId);
+    assertThat(tester.organizations().service().search(new SearchRequest()).getOrganizationsList())
+      .extracting(Organization::getKey)
+      .contains(oldLogin);
+
+    String newLogin = tester.users().generateLogin();
+    authenticate(newLogin, providerId);
+
+    assertThat(tester.users().service().search(new org.sonarqube.ws.client.users.SearchRequest()).getUsersList())
+      .extracting(Users.SearchWsResponse.User::getLogin)
+      .contains(newLogin)
+      .doesNotContainSequence(oldLogin);
+    assertThat(tester.organizations().service().search(new SearchRequest()).getOrganizationsList())
+      .extracting(Organization::getKey)
+      .contains(newLogin)
+      .doesNotContain(oldLogin);
+  }
+
+  @Test
+  public void update_login_and_personal_organization_during_auth_by_updating_only_one_letter_from_lower_case_to_upper_case() {
+    String baseLogin = tester.users().generateLogin();
+    String loginHavingLowerCase = baseLogin + "_letter";
+    String providerId = tester.users().generateProviderId();
+    authenticate(loginHavingLowerCase, providerId);
+    assertThat(tester.organizations().service().search(new SearchRequest()).getOrganizationsList())
+      .extracting(Organization::getKey)
+      .contains(loginHavingLowerCase);
+
+    String loginHavingUpperCase = baseLogin + "_Letter";
+    authenticate(loginHavingUpperCase, providerId);
+
+    assertThat(tester.users().service().search(new org.sonarqube.ws.client.users.SearchRequest()).getUsersList())
+      .extracting(Users.SearchWsResponse.User::getLogin)
+      .contains(loginHavingUpperCase)
+      .doesNotContainSequence(loginHavingLowerCase);
+    assertThat(tester.organizations().service().search(new SearchRequest()).getOrganizationsList())
+      .extracting(Organization::getKey)
+      // Organization key is still using key having lowercase
+      .contains(loginHavingLowerCase)
+      .doesNotContain(loginHavingUpperCase);
+  }
+
+  @Test
+  public void default_assignee_login_is_updated_after_login_update() {
+    String oldLogin = tester.users().generateLogin();
+    String providerId = tester.users().generateProviderId();
+
+    // Create user using authentication, and set user as member of the organization
+    authenticate(oldLogin, providerId);
+    Organization organization = tester.organizations().generate();
+    tester.organizations().service().addMember(new AddMemberRequest().setOrganization(organization.getKey()).setLogin(oldLogin));
+
+    // Set default assignee on project, and execute analysis
+    Projects.CreateWsResponse.Project project = tester.projects().provision(organization);
+    tester.wsClient().settings().set(new SetRequest().setKey("sonar.issues.defaultAssigneeLogin").setComponent(project.getKey()).setValue(oldLogin));
+    Qualityprofiles.CreateWsResponse.QualityProfile profile = tester.qProfiles().createXooProfile(organization);
+    tester.qProfiles().assignQProfileToProject(profile, project);
+    tester.qProfiles().activateRule(profile.getKey(), "xoo:OneIssuePerLine");
+    orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample"),
+      "sonar.organization", organization.getKey(),
+      "sonar.projectKey", project.getKey(),
+      "sonar.login", "admin",
+      "sonar.password", "admin"));
+    tester.wsClient().issues().search(new org.sonarqube.ws.client.issues.SearchRequest().setOrganization(organization.getKey())).getIssuesList()
+      .forEach(i -> assertThat(getIssue(organization, i.getKey()).getAssignee()).isEqualTo(oldLogin));
+
+    // Update login during authentication, check new login is the default assignee
+    String newLogin = tester.users().generateLogin();
+    authenticate(newLogin, providerId);
+    assertThat(tester.wsClient().settings().values(new ValuesRequest().setKeys(singletonList("sonar.issues.defaultAssigneeLogin")).setComponent(project.getKey()))
+      .getSettingsList())
+        .extracting(Settings.Setting::getValue)
+        .containsExactlyInAnyOrder(newLogin);
+  }
+
+  private void authenticate(String login, String providerId) {
+    tester.settings().setGlobalSettings("sonar.auth.fake-base-id-provider.user", login + "," + providerId + ",fake-" + login + ",John,john@email.com");
+    tester.wsClient().wsConnector().call(
+      new GetRequest("/sessions/init/fake-base-id-provider"))
+      .failIfNotSuccessful();
+  }
+
+  private Issue getIssue(Organization organization, String issueKey) {
+    List<Issue> issues = tester.wsClient().issues().search(new org.sonarqube.ws.client.issues.SearchRequest()
+      .setIssues(singletonList(issueKey))
+      .setOrganization(organization.getKey())
+      .setAdditionalFields(singletonList("comments")))
+      .getIssuesList();
+    assertThat(issues).hasSize(1);
+    return issues.get(0);
+  }
+
+}
index 731afead4c9f449a07f674293f228d9ccf2472cc..64011f22d41c433cc37e43ecf8bc3098ca574dc5 100644 (file)
@@ -31,9 +31,11 @@ import static util.ItUtils.xooPlugin;
 
 @RunWith(Suite.class)
 @Suite.SuiteClasses({
-  OrganizationIdentityProviderTest.class,
+  OrganizationBaseIdentityProviderTest.class,
   SonarCloudHomepageTest.class,
-  SonarCloudNotificationsWsTest.class
+  SonarCloudNotificationsWsTest.class,
+  SonarCloudOAuth2IdentityProviderTest.class,
+  SonarCloudUpdateLoginDuringAuthenticationTest.class
 })
 public class SonarCloudUserSuite {
 
@@ -41,13 +43,18 @@ public class SonarCloudUserSuite {
   public static final Orchestrator ORCHESTRATOR = newOrchestratorBuilder()
     .addPlugin(xooPlugin())
 
-    // Used by OrganizationIdentityProviderTest
+    // Used by OrganizationBaseIdentityProviderTest
     .addPlugin(pluginArtifact("base-auth-plugin"))
 
+    // Used in OrganizationOAuth2IdentityProviderTest
+    .addPlugin(pluginArtifact("oauth2-auth-plugin"))
+
     .setServerProperty("sonar.sonarcloud.enabled", "true")
+    .setServerProperty("sonar.organizations.createPersonalOrg", "true")
 
     // reduce memory for Elasticsearch
     .setServerProperty("sonar.search.javaOpts", "-Xms128m -Xmx128m")
+    .setServerProperty("sonar.web.javaAdditionalOpts", "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8001")
 
     .build();