import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
+import static java.util.Objects.requireNonNull;
+
public class NewUser {
private String login;
private String name;
private String email;
private List<String> scmAccounts;
-
private String password;
- private String passwordConfirmation;
+ private ExternalIdentity externalIdentity;
private NewUser() {
// No direct call to this constructor
return password;
}
- public NewUser setPassword(String password) {
+ public NewUser setPassword(@Nullable String password) {
this.password = password;
return this;
}
+ @Nullable
+ public ExternalIdentity externalIdentity() {
+ return externalIdentity;
+ }
+
+ public NewUser setExternalIdentity(@Nullable ExternalIdentity externalIdentity) {
+ this.externalIdentity = externalIdentity;
+ return this;
+ }
+
public static NewUser create() {
return new NewUser();
}
+
+ public static class ExternalIdentity {
+ private String provider;
+ private String id;
+
+ public ExternalIdentity(String provider, String id) {
+ this.provider = requireNonNull(provider, "Identity provider cannot be null");
+ this.id = requireNonNull(id, "Identity id cannot be null");
+ }
+
+ public String getProvider() {
+ return provider;
+ }
+
+ public String getId() {
+ return id;
+ }
+ }
}
package org.sonar.server.user;
import com.google.common.base.Joiner;
+import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
@ServerSide
public class UserUpdater {
+ private static final String SQ_AUTHORITY = "sonarqube";
+
private static final String LOGIN_PARAM = "Login";
private static final String PASSWORD_PARAM = "Password";
private static final String NAME_PARAM = "Name";
}
}
- public boolean create(DbSession dbSession, NewUser newUser){
+ public boolean create(DbSession dbSession, NewUser newUser) {
boolean isUserReactivated = false;
UserDto userDto = createNewUserDto(dbSession, newUser);
String login = userDto.getLogin();
- UserDto existingUser = dbClient.userDao().selectByLogin(dbSession, login);
- if (existingUser == null) {
+ Optional<UserDto> existingUser = dbClient.userDao().selectByExternalIdentity(dbSession, userDto.getExternalIdentity(), userDto.getExternalIdentityProvider());
+ if (!existingUser.isPresent()) {
saveUser(dbSession, userDto);
addDefaultGroup(dbSession, userDto);
} else {
- if (existingUser.isActive()) {
- throw new IllegalArgumentException(String.format("An active user with login '%s' already exists", login));
- }
- UpdateUser updateUser = UpdateUser.create(login)
- .setName(newUser.name())
- .setEmail(newUser.email())
- .setScmAccounts(newUser.scmAccounts())
- .setPassword(newUser.password());
- updateUserDto(dbSession, updateUser, existingUser);
- updateUser(dbSession, existingUser);
- addDefaultGroup(dbSession, existingUser);
- isUserReactivated = true;
+ isUserReactivated = updateExistingUser(dbSession, existingUser.get(), login, newUser);
}
dbSession.commit();
notifyNewUser(userDto.getLogin(), userDto.getName(), newUser.email());
return isUserReactivated;
}
+ private boolean updateExistingUser(DbSession dbSession, UserDto existingUser, String login, NewUser newUser) {
+ if (existingUser.isActive()) {
+ throw new IllegalArgumentException(String.format("An active user with login '%s' already exists", login));
+ }
+ UpdateUser updateUser = UpdateUser.create(login)
+ .setName(newUser.name())
+ .setEmail(newUser.email())
+ .setScmAccounts(newUser.scmAccounts())
+ .setPassword(newUser.password());
+ updateUserDto(dbSession, updateUser, existingUser);
+ updateUser(dbSession, existingUser);
+ addDefaultGroup(dbSession, existingUser);
+ return true;
+ }
+
public void update(UpdateUser updateUser) {
DbSession dbSession = dbClient.openSession(false);
try {
userDto.setScmAccounts(scmAccounts);
}
+ NewUser.ExternalIdentity externalIdentity = newUser.externalIdentity();
+ if (externalIdentity == null) {
+ userDto.setExternalIdentity(login);
+ userDto.setExternalIdentityProvider(SQ_AUTHORITY);
+ } else {
+ userDto.setExternalIdentity(externalIdentity.getId());
+ userDto.setExternalIdentityProvider(externalIdentity.getProvider());
+ }
+
if (!messages.isEmpty()) {
throw new BadRequestException(messages);
}
}
}
- private static void validateLoginFormat(@Nullable String login, List<Message> messages) {
+ private static String validateLoginFormat(@Nullable String login, List<Message> messages) {
checkNotEmptyParam(login, LOGIN_PARAM, messages);
if (!Strings.isNullOrEmpty(login)) {
if (login.length() < LOGIN_MIN_LENGTH) {
messages.add(Message.of("user.bad_login"));
}
}
+ return login;
}
private static void validateNameFormat(@Nullable String name, List<Message> messages) {
}
private void validateScmAccounts(DbSession dbSession, List<String> scmAccounts, @Nullable String login, @Nullable String email, @Nullable UserDto existingUser,
- List<Message> messages) {
+ List<Message> messages) {
for (String scmAccount : scmAccounts) {
if (scmAccount.equals(login) || scmAccount.equals(email)) {
messages.add(Message.of("user.login_or_email_used_as_scm_account"));
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.user;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class NewUserTest {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void create_new_user() throws Exception {
+ NewUser newUser = NewUser.create()
+ .setLogin("login")
+ .setName("name")
+ .setEmail("email")
+ .setPassword("password")
+ .setScmAccounts(asList("login1", "login2"));
+
+ assertThat(newUser.login()).isEqualTo("login");
+ assertThat(newUser.name()).isEqualTo("name");
+ assertThat(newUser.email()).isEqualTo("email");
+ assertThat(newUser.password()).isEqualTo("password");
+ assertThat(newUser.scmAccounts()).contains("login1", "login2");
+ assertThat(newUser.externalIdentity()).isNull();
+ }
+
+ @Test
+ public void create_new_user_with_minimal_fields() throws Exception {
+ NewUser newUser = NewUser.create();
+
+ assertThat(newUser.login()).isNull();
+ assertThat(newUser.name()).isNull();
+ assertThat(newUser.email()).isNull();
+ assertThat(newUser.password()).isNull();
+ assertThat(newUser.scmAccounts()).isNull();
+ }
+
+ @Test
+ public void create_new_user_with_authority() throws Exception {
+ NewUser newUser = NewUser.create()
+ .setLogin("login")
+ .setName("name")
+ .setEmail("email")
+ .setPassword("password")
+ .setExternalIdentity(new NewUser.ExternalIdentity("github", "github_login"));
+
+ assertThat(newUser.externalIdentity().getProvider()).isEqualTo("github");
+ assertThat(newUser.externalIdentity().getId()).isEqualTo("github_login");
+ }
+
+ @Test
+ public void fail_with_NPE_when_identity_provider_is_null() throws Exception {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("Identity provider cannot be null");
+
+ new NewUser.ExternalIdentity(null, "github_login");
+ }
+
+ @Test
+ public void fail_with_NPE_when_identity_id_is_null() throws Exception {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("Identity id cannot be null");
+
+ new NewUser.ExternalIdentity("github", null);
+ }
+}
entry("email", "user@mail.com"));
}
+ @Test
+ public void create_user_with_sq_authority_when_no_authority_set() throws Exception {
+ createDefaultGroup();
+
+ userUpdater.create(NewUser.create()
+ .setLogin("user")
+ .setName("User")
+ .setPassword("password"));
+
+ UserDto dto = userDao.selectByLogin(session, "user");
+ assertThat(dto.getExternalIdentity()).isEqualTo("user");
+ assertThat(dto.getExternalIdentityProvider()).isEqualTo("sonarqube");
+ }
+
+ @Test
+ public void create_user_with_authority() {
+ createDefaultGroup();
+
+ userUpdater.create(NewUser.create()
+ .setLogin("ABCD")
+ .setName("User")
+ .setPassword("password")
+ .setExternalIdentity(new NewUser.ExternalIdentity("github", "user")));
+
+ UserDto dto = userDao.selectByLogin(session, "ABCD");
+ assertThat(dto.getExternalIdentity()).isEqualTo("user");
+ assertThat(dto.getExternalIdentityProvider()).isEqualTo("github");
+ }
+
@Test
public void create_user_with_minimum_fields() {
when(system2.now()).thenReturn(1418215735482L);
import org.sonar.core.permission.GlobalPermissions;
import org.sonar.db.DbSession;
import org.sonar.db.DbTester;
+import org.sonar.db.user.GroupDao;
import org.sonar.db.user.GroupDto;
-import org.sonar.db.user.UserDto;
+import org.sonar.db.user.UserDao;
import org.sonar.db.user.UserGroupDao;
+import org.sonar.db.user.UserTesting;
import org.sonar.server.db.DbClient;
import org.sonar.server.es.EsTester;
import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.user.NewUserNotifier;
import org.sonar.server.user.SecurityRealmFactory;
import org.sonar.server.user.UserUpdater;
-import org.sonar.db.user.GroupDao;
-import org.sonar.db.user.UserDao;
import org.sonar.server.user.index.UserDoc;
import org.sonar.server.user.index.UserIndex;
import org.sonar.server.user.index.UserIndexDefinition;
public void reactivate_user() throws Exception {
userSessionRule.login("admin").setLocale(Locale.FRENCH).setGlobalPermissions(GlobalPermissions.SYSTEM_ADMIN);
- dbClient.userDao().insert(session, new UserDto()
- .setEmail("john@email.com")
- .setLogin("john")
- .setName("John")
- .setActive(true));
+ dbClient.userDao().insert(session, UserTesting.newUserDto("john", "John", "john@email.com"));
session.commit();
dbClient.userDao().deactivateUserByLogin(session, "john");
userIndexer.index();
<dataset>
- <users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[false]" scm_accounts=" ma marius33 " created_at="1418215735482" updated_at="1418215735485"
+ <users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[false]" scm_accounts=" ma marius33 "
+ external_identity_provider="sonarqube" external_identity="marius"
+ created_at="1418215735482" updated_at="1418215735485"
salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
<groups id="1" name="sonar-devs" description="Sonar Devs" created_at="2014-09-08" updated_at="2014-09-08"/>
<dataset>
- <users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[true]" scm_accounts=" ma marius33 " created_at="1418215735482" updated_at="1418215735485"
+ <users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[true]" scm_accounts=" ma marius33 "
+ external_identity_provider="sonarqube" external_identity="marius"
+ created_at="1418215735482" updated_at="1418215735485"
salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
</dataset>
<dataset>
- <users id="101" login="john" name="John" email="john@email.com" active="[true]" scm_accounts=" jo " created_at="1418215735482" updated_at="1418215735485"
+ <users id="101" login="john" name="John" email="john@email.com" active="[true]" scm_accounts=" jo "
+ external_identity_provider="sonarqube" external_identity="john"
+ created_at="1418215735482" updated_at="1418215735485"
salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
</dataset>
<dataset>
- <users id="101" login="john" name="John" email="john@email.com" active="[true]" scm_accounts=" jo " created_at="1418215735482" updated_at="1418215735485"
+ <users id="101" login="john" name="John" email="john@email.com" active="[true]" scm_accounts=" jo "
+ external_identity_provider="sonarqube" external_identity="john"
+ created_at="1418215735482" updated_at="1418215735485"
salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
- <users id="102" login="technical-account" name="Technical account" email="john@email.com" active="[true]" scm_accounts="[null]" created_at="1418215735482"
- updated_at="1418215735485"
+ <users id="102" login="technical-account" name="Technical account" email="john@email.com" active="[true]" scm_accounts="[null]"
+ external_identity_provider="sonarqube" external_identity="technical-account"
+ created_at="1418215735482" updated_at="1418215735485"
salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
</dataset>
<dataset>
- <users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[true]" scm_accounts=" ma marius33 " created_at="1418215735482" updated_at="1418215735485"
+ <users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[true]" scm_accounts=" ma marius33 "
+ external_identity_provider="sonarqube" external_identity="marius"
+ created_at="1418215735482" updated_at="1418215735485"
salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
</dataset>
<dataset>
- <users id="101" login="john" name="John" email="john@email.com" active="[true]" scm_accounts=" jo " created_at="1418215735482" updated_at="1418215735485"
+ <users id="101" login="john" name="John" email="john@email.com" active="[true]" scm_accounts=" jo "
+ external_identity_provider="sonarqube" external_identity="john"
+ created_at="1418215735482" updated_at="1418215735485"
salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
- <users id="102" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[true]" scm_accounts=" ma marius33 " created_at="1418215735482" updated_at="1418215735485"
+ <users id="102" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[true]" scm_accounts=" ma marius33 "
+ external_identity_provider="sonarqube" external_identity="marius"
+ created_at="1418215735482" updated_at="1418215735485"
salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
</dataset>
<dataset>
- <users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[true]" scm_accounts=" ma marius33 " created_at="1418215735482" updated_at="1418215735485"
+ <users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[true]" scm_accounts=" ma marius33 "
+ external_identity_provider="sonarqube" external_identity="marius"
+ created_at="1418215735482" updated_at="1418215735485"
salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
<groups id="1" name="sonar-users" description="Sonar Users" created_at="2014-09-08" updated_at="2014-09-08"/>
<dataset>
- <users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[false]" scm_accounts=" ma marius33 " created_at="1418215735482" updated_at="1418215735485"
+ <users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[false]" scm_accounts=" ma marius33 "
+ external_identity_provider="sonarqube" external_identity="marius"
+ created_at="1418215735482" updated_at="1418215735485"
salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
</dataset>
<dataset>
<users id="101" login="tech_user" name="Tech user" email="tech@user.fr" active="[true]" scm_accounts="[null]"
+ external_identity_provider="sonarqube" external_identity="tech_user"
created_at="1418215735482" updated_at="1418215735485"
salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
<dataset>
- <users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[true]" scm_accounts=" ma marius33 " created_at="1418215735482" updated_at="1418215735485"
+ <users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[true]" scm_accounts=" ma marius33 "
+ external_identity_provider="sonarqube" external_identity="marius"
+ created_at="1418215735482" updated_at="1418215735485"
salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
</dataset>
--- /dev/null
+#
+# SonarQube, open source software quality management tool.
+# Copyright (C) 2008-2014 SonarSource
+# mailto:contact AT sonarsource DOT com
+#
+# SonarQube is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 3 of the License, or (at your option) any later version.
+#
+# SonarQube is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+
+#
+# SonarQube 5.4
+# SONAR-6226
+#
+class AddUsersIdentityColumns < ActiveRecord::Migration
+
+ def self.up
+ execute_java_migration('org.sonar.db.version.v54.AddUsersIdentityColumns')
+ end
+
+end
--- /dev/null
+#
+# SonarQube, open source software quality management tool.
+# Copyright (C) 2008-2014 SonarSource
+# mailto:contact AT sonarsource DOT com
+#
+# SonarQube is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 3 of the License, or (at your option) any later version.
+#
+# SonarQube is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+
+#
+# SonarQube 5.4
+# SONAR-6226
+#
+class MigrateUsersIdentity < ActiveRecord::Migration
+
+ def self.up
+ execute_java_migration('org.sonar.db.version.v54.MigrateUsersIdentity')
+ end
+
+end
--- /dev/null
+#
+# SonarQube, open source software quality management tool.
+# Copyright (C) 2008-2014 SonarSource
+# mailto:contact AT sonarsource DOT com
+#
+# SonarQube is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 3 of the License, or (at your option) any later version.
+#
+# SonarQube is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+
+#
+# SonarQube 5.4
+# SONAR-6226
+#
+class AddUniqueIndexOnUsersIdentityColumns < ActiveRecord::Migration
+
+ def self.up
+ add_index :users, [:external_identity, :external_identity_provider], :name => 'uniq_users_identity'
+ end
+
+end
package org.sonar.db.user;
import com.google.common.base.Function;
+import com.google.common.base.Optional;
import java.util.Collection;
import java.util.List;
import javax.annotation.CheckForNull;
import org.sonar.db.MyBatis;
import org.sonar.db.RowNotFoundException;
+import static com.google.common.base.Optional.fromNullable;
+
public class UserDao implements Dao {
private final MyBatis mybatis;
return mapper(session).selectNullableByScmAccountOrLoginOrEmail(scmAccountOrLoginOrEmail, like);
}
+ public Optional<UserDto> selectByExternalIdentity(DbSession session, String extIdentity, String extIdentityProvider){
+ return fromNullable(mapper(session).selectByIdentity(extIdentity, extIdentityProvider));
+ }
+
+ public UserDto selectOrFailByExternalIdentity(DbSession session, String extIdentity, String extIdentityProvider) {
+ Optional<UserDto> user = selectByExternalIdentity(session, extIdentity, extIdentityProvider);
+ if (user.isPresent()) {
+ return user.get();
+ }
+ throw new RowNotFoundException(String.format("User with identity provider '%s' and id '%s' has not been found", extIdentityProvider, extIdentity));
+ }
+
protected UserMapper mapper(DbSession session) {
return session.getMapper(UserMapper.class);
}
private String email;
private boolean active = true;
private String scmAccounts;
+ private String externalIdentity;
+ private String externalIdentityProvider;
private String cryptedPassword;
private String salt;
private Long createdAt;
}
}
+ public String getExternalIdentity() {
+ return externalIdentity;
+ }
+
+ public UserDto setExternalIdentity(String authorithy) {
+ this.externalIdentity = authorithy;
+ return this;
+ }
+
+ public String getExternalIdentityProvider() {
+ return externalIdentityProvider;
+ }
+
+ public UserDto setExternalIdentityProvider(String externalIdentityProvider) {
+ this.externalIdentityProvider = externalIdentityProvider;
+ return this;
+ }
+
public String getCryptedPassword() {
return cryptedPassword;
}
List<UserDto> selectByLogins(List<String> logins);
+ @CheckForNull
+ UserDto selectByIdentity(@Param("extIdentity") String authorityId, @Param("extIdentityProvider") String authorityProvider);
+
@CheckForNull
GroupDto selectGroupByName(String name);
public class DatabaseVersion {
- public static final int LAST_VERSION = 1010;
+ public static final int LAST_VERSION = 1013;
/**
* The minimum supported version which can be upgraded. Lower
import org.sonar.db.version.v52.RemoveSnapshotLibraries;
import org.sonar.db.version.v53.FixMsSqlCollation;
import org.sonar.db.version.v53.UpdateCustomDashboardInLoadedTemplates;
+import org.sonar.db.version.v54.AddUsersIdentityColumns;
import org.sonar.db.version.v54.InsertGateAdminPermissionForEachProfileAdmin;
+import org.sonar.db.version.v54.MigrateUsersIdentity;
import org.sonar.db.version.v54.RemoveComponentPageProperties;
public class MigrationStepModule extends Module {
// 5.4
InsertGateAdminPermissionForEachProfileAdmin.class,
- RemoveComponentPageProperties.class);
+ RemoveComponentPageProperties.class,
+ AddUsersIdentityColumns.class,
+ MigrateUsersIdentity.class
+ );
}
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.db.version.v54;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.db.version.AddColumnsBuilder;
+import org.sonar.db.version.DdlChange;
+
+import static org.sonar.db.version.VarcharColumnDef.newVarcharColumnDefBuilder;
+
+/**
+ * Add the following columns to the USERS table :
+ * - external_identity
+ * - external_identity_provider
+ */
+public class AddUsersIdentityColumns extends DdlChange {
+
+ private final Database db;
+
+ public AddUsersIdentityColumns(Database db) {
+ super(db);
+ this.db = db;
+ }
+
+ @Override
+ public void execute(DdlChange.Context context) throws SQLException {
+ context.execute(generateSql());
+ }
+
+ private String generateSql() {
+ return new AddColumnsBuilder(db.getDialect(), "users")
+ .addColumn(newVarcharColumnDefBuilder().setColumnName("external_identity").setLimit(4000).setIsNullable(true).build())
+ .addColumn(newVarcharColumnDefBuilder().setColumnName("external_identity_provider").setLimit(100).setIsNullable(true).build())
+ .build();
+ }
+
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.db.version.v54;
+
+import java.sql.SQLException;
+import org.sonar.api.utils.System2;
+import org.sonar.db.Database;
+import org.sonar.db.version.BaseDataChange;
+import org.sonar.db.version.MassUpdate;
+import org.sonar.db.version.Select;
+import org.sonar.db.version.SqlStatement;
+
+/**
+ * Update all users to feed external_identity_provider with 'sonarqube' and external_identity with the login
+ */
+public class MigrateUsersIdentity extends BaseDataChange {
+
+ private final System2 system2;
+
+ public MigrateUsersIdentity(Database db, System2 system2) {
+ super(db);
+ this.system2 = system2;
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ MassUpdate update = context.prepareMassUpdate().rowPluralName("users");
+ update.select("SELECT u.id, u.login FROM users u");
+ update.update("UPDATE users SET external_identity_provider=?, external_identity=?, updated_at=? WHERE id=? " +
+ "AND external_identity_provider IS NULL AND external_identity IS NULL");
+ update.execute(new MigrationHandler(system2.now()));
+ }
+
+ private static class MigrationHandler implements MassUpdate.Handler {
+ private final long now;
+
+ public MigrationHandler(long now) {
+ this.now = now;
+ }
+
+ @Override
+ public boolean handle(Select.Row row, SqlStatement update) throws SQLException {
+ update.setString(1, "sonarqube");
+ update.setString(2, row.getString(2));
+ update.setLong(3, now);
+ update.setLong(4, row.getLong(1));
+ return true;
+ }
+ }
+}
u.scm_accounts as "scmAccounts",
u.salt as "salt",
u.crypted_password as "cryptedPassword",
+ u.external_identity as "externalIdentity",
+ u.external_identity_provider as "externalIdentityProvider",
u.created_at as "createdAt",
u.updated_at as "updatedAt"
</sql>
ORDER BY u.name
</select>
+ <select id="selectByIdentity" parameterType="map" resultType="User">
+ SELECT <include refid="userColumns"/>
+ FROM users u
+ <where>
+ u.external_identity=#{extIdentity}
+ AND u.external_identity_provider=#{extIdentityProvider}
+ </where>
+ </select>
+
<select id="selectGroupByName" parameterType="string" resultType="Group">
SELECT id, name, description, created_at AS "createdAt", updated_at AS "updatedAt"
FROM groups WHERE name=#{id}
</update>
<insert id="insert" parameterType="User" keyColumn="id" useGeneratedKeys="true" keyProperty="id">
- INSERT INTO users (login, name, email, active, scm_accounts, salt, crypted_password, created_at, updated_at)
+ INSERT INTO users (login, name, email, active, scm_accounts, external_identity, external_identity_provider, salt, crypted_password, created_at, updated_at)
VALUES (#{login,jdbcType=VARCHAR}, #{name,jdbcType=VARCHAR}, #{email,jdbcType=VARCHAR}, #{active,jdbcType=BOOLEAN},
- #{scmAccounts,jdbcType=VARCHAR},
+ #{scmAccounts,jdbcType=VARCHAR}, #{externalIdentity,jdbcType=VARCHAR}, #{externalIdentityProvider,jdbcType=VARCHAR},
#{salt,jdbcType=VARCHAR}, #{cryptedPassword,jdbcType=VARCHAR}, #{createdAt,jdbcType=BIGINT},
#{updatedAt,jdbcType=BIGINT})
</insert>
INSERT INTO SCHEMA_MIGRATIONS(VERSION) VALUES ('1008');
INSERT INTO SCHEMA_MIGRATIONS(VERSION) VALUES ('1009');
INSERT INTO SCHEMA_MIGRATIONS(VERSION) VALUES ('1010');
+INSERT INTO SCHEMA_MIGRATIONS(VERSION) VALUES ('1011');
+INSERT INTO SCHEMA_MIGRATIONS(VERSION) VALUES ('1012');
+INSERT INTO SCHEMA_MIGRATIONS(VERSION) VALUES ('1013');
INSERT INTO USERS(ID, LOGIN, NAME, EMAIL, CRYPTED_PASSWORD, SALT, CREATED_AT, UPDATED_AT, REMEMBER_TOKEN, REMEMBER_TOKEN_EXPIRES_AT) VALUES (1, 'admin', 'Administrator', '', 'a373a0e667abb2604c1fd571eb4ad47fe8cc0878', '48bc4b0d93179b5103fd3885ea9119498e9d161b', '1418215735482', '1418215735482', null, null);
ALTER TABLE USERS ALTER COLUMN ID RESTART WITH 2;
"REMEMBER_TOKEN_EXPIRES_AT" TIMESTAMP,
"ACTIVE" BOOLEAN DEFAULT TRUE,
"SCM_ACCOUNTS" VARCHAR(4000),
+ "EXTERNAL_IDENTITY" VARCHAR(4000),
+ "EXTERNAL_IDENTITY_PROVIDER" VARCHAR(100),
"CREATED_AT" BIGINT,
"UPDATED_AT" BIGINT
);
CREATE UNIQUE INDEX "USERS_LOGIN" ON "USERS" ("LOGIN");
+CREATE UNIQUE INDEX "UNIQ_USERS_IDENTITY" ON "USERS" ("EXTERNAL_IDENTITY", "EXTERNAL_IDENTITY_PROVIDER");
+
CREATE INDEX "USERS_UPDATED_AT" ON "USERS" ("UPDATED_AT");
CREATE INDEX "SNAPSHOTS_ROOT_PROJECT_ID" ON "SNAPSHOTS" ("ROOT_PROJECT_ID");
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.categories.Category;
+import org.junit.rules.ExpectedException;
import org.sonar.api.user.UserQuery;
import org.sonar.api.utils.DateUtils;
import org.sonar.api.utils.System2;
import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.guava.api.Assertions.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@Category(DbTests.class)
public class UserDaoTest {
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
System2 system2 = mock(System2.class);
@Rule
.setActive(true)
.setSalt("1234")
.setCryptedPassword("abcd")
+ .setExternalIdentity("johngithub")
+ .setExternalIdentityProvider("github")
.setCreatedAt(date)
.setUpdatedAt(date);
underTest.insert(db.getSession(), userDto);
assertThat(user.getScmAccounts()).isEqualTo(",jo.hn,john2,");
assertThat(user.getSalt()).isEqualTo("1234");
assertThat(user.getCryptedPassword()).isEqualTo("abcd");
+ assertThat(user.getExternalIdentity()).isEqualTo("johngithub");
+ assertThat(user.getExternalIdentityProvider()).isEqualTo("github");
assertThat(user.getCreatedAt()).isEqualTo(date);
assertThat(user.getUpdatedAt()).isEqualTo(date);
}
assertThat(underTest.selectByLogin(session, "unknown")).isNull();
}
+
+ @Test
+ public void select_user_by_external_identity() {
+ db.prepareDbUnit(getClass(), "select_users_by_ext_identity.xml");
+
+ assertThat(underTest.selectByExternalIdentity(session, "mariusgithub", "github")).isPresent();
+ assertThat(underTest.selectByExternalIdentity(session, "mariusgithub", "google")).isAbsent();
+ assertThat(underTest.selectByExternalIdentity(session, "unknown", "unknown")).isAbsent();
+ }
+
+ @Test
+ public void select_or_fail_by_external_identity() throws Exception {
+ db.prepareDbUnit(getClass(), "select_users_by_ext_identity.xml");
+ assertThat(underTest.selectOrFailByExternalIdentity(session, "mariusgithub", "github")).isNotNull();
+
+ thrown.expect(RowNotFoundException.class);
+ thrown.expectMessage("User with identity provider 'unknown' and id 'unknown' has not been found");
+ underTest.selectOrFailByExternalIdentity(session, "unknown", "unknown");
+ }
}
*/
package org.sonar.db.user;
-import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
import static org.apache.commons.lang.math.RandomUtils.nextLong;
public class UserTesting {
public static UserDto newUserDto() {
- UserDto user = new UserDto()
+ return newUserDto(randomAlphanumeric(30), randomAlphanumeric(30), randomAlphanumeric(30));
+ }
+
+ public static UserDto newUserDto(String login, String name, String email) {
+ return new UserDto()
.setActive(true)
- .setName(randomAlphanumeric(30))
- .setEmail(randomAlphabetic(30))
- .setLogin(randomAlphanumeric(30));
- user.setCreatedAt(nextLong())
+ .setName(name)
+ .setEmail(email)
+ .setLogin(login)
+ .setExternalIdentity(login)
+ .setExternalIdentityProvider("sonarqube")
+ .setCreatedAt(nextLong())
.setUpdatedAt(nextLong());
- return user;
}
}
public void verify_count_of_added_MigrationStep_types() {
ComponentContainer container = new ComponentContainer();
new MigrationStepModule().configure(container);
- assertThat(container.size()).isEqualTo(45);
+ assertThat(container.size()).isEqualTo(47);
}
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.db.version.v54;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbTester;
+import org.sonar.db.version.MigrationStep;
+
+import static java.sql.Types.VARCHAR;
+
+public class AddUsersIdentityColumnsTest {
+
+ @Rule
+ public DbTester db = DbTester.createForSchema(System2.INSTANCE, AddUsersIdentityColumnsTest.class, "schema.sql");
+
+ MigrationStep migration;
+
+ @Before
+ public void setUp() {
+ migration = new AddUsersIdentityColumns(db.database());
+ }
+
+ @Test
+ public void update_columns() throws Exception {
+ migration.execute();
+
+ db.assertColumnDefinition("users", "external_identity_provider", VARCHAR, 100);
+ db.assertColumnDefinition("users", "external_identity", VARCHAR, 4000);
+ }
+
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.db.version.v54;
+
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbTester;
+import org.sonar.db.version.MigrationStep;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class MigrateUsersIdentityTest {
+
+ static final String TABLE = "users";
+
+ static final long NOW = 1500000000000L;
+
+ final System2 system2 = mock(System2.class);
+
+ @Rule
+ public DbTester db = DbTester.createForSchema(System2.INSTANCE, MigrateUsersIdentityTest.class, "schema.sql");
+
+ MigrationStep migration;
+
+ @Before
+ public void setUp() {
+ db.executeUpdateSql("truncate table " + TABLE);
+ when(system2.now()).thenReturn(NOW);
+ migration = new MigrateUsersIdentity(db.database(), system2);
+ }
+
+ @Test
+ public void migrate_empty_db() throws Exception {
+ migration.execute();
+ }
+
+ @Test
+ public void migrate() throws Exception {
+ db.prepareDbUnit(this.getClass(), "migrate.xml");
+
+ migration.execute();
+
+ assertThat(db.countRowsOfTable(TABLE)).isEqualTo(2);
+ assertUser(101, "john", NOW);
+ assertUser(102, "arthur", NOW);
+ }
+
+ @Test
+ public void nothing_to_do_on_already_migrated_data() throws Exception {
+ db.prepareDbUnit(this.getClass(), "migrate-result.xml");
+
+ migration.execute();
+
+ assertThat(db.countRowsOfTable(TABLE)).isEqualTo(2);
+ assertUser(101, "john", 1418215735485L);
+ assertUser(102, "arthur", 1418215735485L);
+ }
+
+ private void assertUser(long userId, String expectedAuthorityId, long expectedUpdatedAt) {
+ Map<String, Object> result = db.selectFirst("SELECT u.external_identity as \"externalIdentity\", " +
+ "u.external_identity_provider as \"externalIdentityProvider\", " +
+ "u.updated_at as \"updatedAt\" " +
+ "FROM users u WHERE u.id=" + userId);
+ assertThat(result.get("externalIdentity")).isEqualTo(expectedAuthorityId);
+ assertThat(result.get("externalIdentityProvider")).isEqualTo("sonarqube");
+ assertThat(result.get("updatedAt")).isEqualTo(expectedUpdatedAt);
+ }
+
+}
--- /dev/null
+<dataset>
+
+ <users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[true]" scm_accounts=" ma marius33 "
+ external_identity_provider="github" external_identity="mariusgithub"
+ created_at="1418215735482" updated_at="1418215735485"
+ salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
+ <users id="102" login="sbrandhof" name="Simon Brandhof" email="marius@lesbronzes.fr" active="[true]" scm_accounts="[null]"
+ external_identity_provider="google" external_identity="mariusgoogle"
+ created_at="1418215735482" updated_at="1418215735485"
+ salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8366" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fh"/>
+
+</dataset>
--- /dev/null
+CREATE TABLE "USERS" (
+ "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1),
+ "LOGIN" VARCHAR(255),
+ "NAME" VARCHAR(200),
+ "EMAIL" VARCHAR(100),
+ "CRYPTED_PASSWORD" VARCHAR(40),
+ "SALT" VARCHAR(40),
+ "REMEMBER_TOKEN" VARCHAR(500),
+ "REMEMBER_TOKEN_EXPIRES_AT" TIMESTAMP,
+ "ACTIVE" BOOLEAN DEFAULT TRUE,
+ "SCM_ACCOUNTS" VARCHAR(4000),
+ "CREATED_AT" BIGINT,
+ "UPDATED_AT" BIGINT
+);
--- /dev/null
+<dataset>
+
+ <users id="101" login="john" name="John" email="john@email.com" active="[true]" scm_accounts=" jo "
+ external_identity="john" external_identity_provider="sonarqube"
+ created_at="1418215735482" updated_at="1418215735485"
+ salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
+
+ <users id="102" login="arthur" name="Arthur" email="arthur@email.com" active="[false]" scm_accounts=""
+ external_identity="arthur" external_identity_provider="sonarqube"
+ created_at="1418215735482" updated_at="1418215735485"
+ salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
+
+</dataset>
--- /dev/null
+<dataset>
+
+ <users id="101" login="john" name="John" email="john@email.com" active="[true]" scm_accounts=" jo "
+ external_identity="[null]" external_identity_provider="[null]"
+ created_at="1418215735482" updated_at="1418215735485"
+ salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
+
+ <users id="102" login="arthur" name="Arthur" email="arthur@email.com" active="[false]" scm_accounts=""
+ external_identity="[null]" external_identity_provider="[null]"
+ created_at="1418215735482" updated_at="1418215735485"
+ salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
+
+</dataset>
--- /dev/null
+CREATE TABLE "USERS" (
+ "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1),
+ "LOGIN" VARCHAR(255),
+ "NAME" VARCHAR(200),
+ "EMAIL" VARCHAR(100),
+ "CRYPTED_PASSWORD" VARCHAR(40),
+ "SALT" VARCHAR(40),
+ "REMEMBER_TOKEN" VARCHAR(500),
+ "REMEMBER_TOKEN_EXPIRES_AT" TIMESTAMP,
+ "ACTIVE" BOOLEAN DEFAULT TRUE,
+ "SCM_ACCOUNTS" VARCHAR(4000),
+ "EXTERNAL_IDENTITY_PROVIDER" VARCHAR(4000),
+ "EXTERNAL_IDENTITY" VARCHAR(100),
+ "CREATED_AT" BIGINT,
+ "UPDATED_AT" BIGINT
+);