From 0cd6464c0bd4a472dc439a5cb62841ed48b9be9e Mon Sep 17 00:00:00 2001 From: Julien Lancelot Date: Wed, 16 Mar 2016 17:01:02 +0100 Subject: [PATCH] SONAR-7448 Allow groups synchronization in IdentityProvider API --- .../UserIdentityAuthenticator.java | 119 ++++++-- .../usergroups/ws/UserGroupUpdater.java | 4 +- .../UserIdentityAuthenticatorTest.java | 258 ++++++++++++++---- .../main/java/org/sonar/db/user/GroupDao.java | 22 ++ .../java/org/sonar/db/user/GroupMapper.java | 2 + .../org/sonar/db/user/GroupMapper.xml | 12 + .../java/org/sonar/db/user/GroupDaoTest.java | 16 ++ .../server/authentication/UserIdentity.java | 73 ++++- .../sonar/api/user/UserGroupValidation.java | 40 +++ .../authentication/UserIdentityTest.java | 96 +++++++ .../api/user/UserGroupValidationTest.java | 71 +++++ 11 files changed, 638 insertions(+), 75 deletions(-) create mode 100644 sonar-plugin-api/src/main/java/org/sonar/api/user/UserGroupValidation.java create mode 100644 sonar-plugin-api/src/test/java/org/sonar/api/user/UserGroupValidationTest.java diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticator.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticator.java index 643daf69091..b0efc4e7d24 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticator.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticator.java @@ -19,22 +19,38 @@ */ package org.sonar.server.authentication; +import com.google.common.base.Function; +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.Set; +import javax.annotation.Nonnull; import javax.servlet.http.HttpSession; import org.sonar.api.server.authentication.IdentityProvider; import org.sonar.api.server.authentication.UnauthorizedException; 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.user.ExternalIdentity; import org.sonar.server.user.NewUser; import org.sonar.server.user.UpdateUser; import org.sonar.server.user.UserUpdater; +import static com.google.common.collect.FluentIterable.from; import static java.lang.String.format; +import static java.util.Collections.singletonList; public class UserIdentityAuthenticator { + private static final Logger LOGGER = Loggers.get(UserIdentityAuthenticator.class); + private final DbClient dbClient; private final UserUpdater userUpdater; @@ -56,34 +72,97 @@ public class UserIdentityAuthenticator { String userLogin = user.getLogin(); UserDto userDto = dbClient.userDao().selectByLogin(dbSession, userLogin); if (userDto != null && userDto.isActive()) { - userUpdater.update(dbSession, UpdateUser.create(userDto.getLogin()) - .setEmail(user.getEmail()) - .setName(user.getName()) - .setExternalIdentity(new ExternalIdentity(provider.getKey(), user.getProviderLogin())) - .setPassword(null)); + registerExistingUser(dbSession, userDto, user, provider); return userDto.getId(); } + return registerNewUser(dbSession, user, provider); + } finally { + dbClient.closeSession(dbSession); + } + } + + private long registerNewUser(DbSession dbSession, UserIdentity user, IdentityProvider provider) { + if (!provider.allowsUsersToSignUp()) { + throw new UnauthorizedException(format("'%s' users are not allowed to sign up", provider.getKey())); + } + + String email = user.getEmail(); + if (email != null && dbClient.userDao().doesEmailExist(dbSession, email)) { + throw new UnauthorizedException(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)); + } + + String userLogin = user.getLogin(); + userUpdater.create(dbSession, NewUser.create() + .setLogin(userLogin) + .setEmail(user.getEmail()) + .setName(user.getName()) + .setExternalIdentity(new ExternalIdentity(provider.getKey(), user.getProviderLogin())) + ); + UserDto newUser = dbClient.userDao().selectOrFailByLogin(dbSession, userLogin); + syncGroups(dbSession, user, newUser); + return newUser.getId(); + } + + private void registerExistingUser(DbSession dbSession, UserDto userDto, UserIdentity user, IdentityProvider provider) { + userUpdater.update(dbSession, UpdateUser.create(userDto.getLogin()) + .setEmail(user.getEmail()) + .setName(user.getName()) + .setExternalIdentity(new ExternalIdentity(provider.getKey(), user.getProviderLogin())) + .setPassword(null)); + syncGroups(dbSession, user, userDto); + } - if (!provider.allowsUsersToSignUp()) { - throw new UnauthorizedException(format("'%s' users are not allowed to sign up", provider.getKey())); + private void syncGroups(DbSession dbSession, UserIdentity userIdentity, UserDto userDto) { + if (userIdentity.shouldSyncGroups()) { + String userLogin = userIdentity.getLogin(); + Set userGroups = new HashSet<>(dbClient.groupMembershipDao().selectGroupsByLogins(dbSession, singletonList(userLogin)).get(userLogin)); + Set identityGroups = userIdentity.getGroups(); + LOGGER.debug("List of groups returned by the identity provider '{}'", identityGroups); + + Collection groupsToAdd = Sets.difference(identityGroups, userGroups); + Collection groupsToRemove = Sets.difference(userGroups, identityGroups); + Collection allGroups = new ArrayList<>(groupsToAdd); + allGroups.addAll(groupsToRemove); + Map groupsByName = from(dbClient.groupDao().selectByNames(dbSession, allGroups)).uniqueIndex(GroupDtoToName.INSTANCE); + + addGroups(dbSession, userDto, groupsToAdd, groupsByName); + removeGroups(dbSession, userDto, groupsToRemove, groupsByName); + + dbSession.commit(); + } + } + + private void addGroups(DbSession dbSession, UserDto userDto, Collection groupsToAdd, Map groupsByName) { + if (!groupsToAdd.isEmpty()) { + for (String groupToAdd : groupsToAdd) { + GroupDto groupDto = groupsByName.get(groupToAdd); + if (groupDto != null) { + LOGGER.debug("Adding group '{}' to user '{}'", groupDto.getName(), userDto.getLogin()); + dbClient.userGroupDao().insert(dbSession, new UserGroupDto().setGroupId(groupDto.getId()).setUserId(userDto.getId())); + } } + } + } - String email = user.getEmail(); - if (email != null && dbClient.userDao().doesEmailExist(dbSession, email)) { - throw new UnauthorizedException(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)); + private void removeGroups(DbSession dbSession, UserDto userDto, Collection groupsToRemove, Map groupsByName) { + if (!groupsToRemove.isEmpty()) { + for (String groupToRemove : groupsToRemove) { + GroupDto groupDto = groupsByName.get(groupToRemove); + if (groupDto != null) { + LOGGER.debug("Removing group '{}' from user '{}'", groupDto.getName(), userDto.getLogin()); + dbClient.userGroupDao().delete(dbSession, new UserGroupDto().setGroupId(groupDto.getId()).setUserId(userDto.getId())); + } } + } + } - userUpdater.create(dbSession, NewUser.create() - .setLogin(userLogin) - .setEmail(user.getEmail()) - .setName(user.getName()) - .setExternalIdentity(new ExternalIdentity(provider.getKey(), user.getProviderLogin())) - ); - return dbClient.userDao().selectOrFailByLogin(dbSession, userLogin).getId(); + private enum GroupDtoToName implements Function { + INSTANCE; - } finally { - dbClient.closeSession(dbSession); + @Override + public String apply(@Nonnull GroupDto input) { + return input.getName(); } } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/usergroups/ws/UserGroupUpdater.java b/server/sonar-server/src/main/java/org/sonar/server/usergroups/ws/UserGroupUpdater.java index ac68f295357..ae2e10adaed 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/usergroups/ws/UserGroupUpdater.java +++ b/server/sonar-server/src/main/java/org/sonar/server/usergroups/ws/UserGroupUpdater.java @@ -22,6 +22,7 @@ package org.sonar.server.usergroups.ws; import com.google.common.base.Preconditions; import org.sonar.api.security.DefaultGroups; import org.sonar.api.server.ServerSide; +import org.sonar.api.user.UserGroupValidation; import org.sonar.api.utils.text.JsonWriter; import org.sonar.db.DbClient; import org.sonar.db.DbSession; @@ -48,8 +49,7 @@ public class UserGroupUpdater { } protected void validateName(String name) { - checkNameLength(name); - checkNameNotAnyone(name); + UserGroupValidation.validateGroupName(name); } private static void checkNameLength(String name) { 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 index d0cfc2f72c1..9248fdf0419 100644 --- 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 @@ -19,33 +19,42 @@ */ package org.sonar.server.authentication; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; import javax.servlet.http.HttpSession; 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.Settings; import org.sonar.api.server.authentication.UnauthorizedException; import org.sonar.api.server.authentication.UserIdentity; +import org.sonar.api.utils.System2; import org.sonar.db.DbClient; 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.UserDao; import org.sonar.db.user.UserDto; -import org.sonar.server.user.NewUser; -import org.sonar.server.user.UpdateUser; +import org.sonar.db.user.UserGroupDto; +import org.sonar.db.user.UserTesting; +import org.sonar.server.user.NewUserNotifier; import org.sonar.server.user.UserUpdater; +import org.sonar.server.user.index.UserIndexer; +import static com.google.common.collect.Sets.newHashSet; +import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; public class UserIdentityAuthenticatorTest { static String USER_LOGIN = "github-johndoo"; - static UserDto ACTIVE_USER = new UserDto().setId(10L).setLogin(USER_LOGIN).setActive(true); - static UserDto UNACTIVE_USER = new UserDto().setId(11L).setLogin("UNACTIVE").setActive(false); + + static String DEFAULT_GROUP = "default"; static UserIdentity USER_IDENTITY = UserIdentity.builder() .setProviderLogin("johndoo") @@ -62,83 +71,213 @@ public class UserIdentityAuthenticatorTest { @Rule public ExpectedException thrown = ExpectedException.none(); - DbClient dbClient = mock(DbClient.class); - DbSession dbSession = mock(DbSession.class); - UserDao userDao = mock(UserDao.class); + System2 system2 = mock(System2.class); + + @Rule + public DbTester dbTester = DbTester.create(system2); + + DbClient dbClient = dbTester.getDbClient(); + DbSession dbSession = dbTester.getSession(); + UserDao userDao = dbClient.userDao(); + GroupDao groupDao = dbClient.groupDao(); + Settings settings = new Settings(); HttpSession httpSession = mock(HttpSession.class); - UserUpdater userUpdater = mock(UserUpdater.class); + UserUpdater userUpdater = new UserUpdater( + mock(NewUserNotifier.class), + settings, + dbClient, + mock(UserIndexer.class), + system2 + ); UserIdentityAuthenticator underTest = new UserIdentityAuthenticator(dbClient, userUpdater); @Before public void setUp() throws Exception { - when(dbClient.openSession(false)).thenReturn(dbSession); - when(dbClient.userDao()).thenReturn(userDao); + settings.setProperty("sonar.defaultGroup", DEFAULT_GROUP); + addGroup(DEFAULT_GROUP); } @Test public void authenticate_new_user() throws Exception { - when(userDao.selectByLogin(dbSession, USER_IDENTITY.getLogin())).thenReturn(null); - when(userDao.selectOrFailByLogin(dbSession, USER_IDENTITY.getLogin())).thenReturn(ACTIVE_USER); - underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, httpSession); + dbSession.commit(); - ArgumentCaptor newUserArgumentCaptor = ArgumentCaptor.forClass(NewUser.class); - verify(userUpdater).create(eq(dbSession), newUserArgumentCaptor.capture()); - NewUser newUser = newUserArgumentCaptor.getValue(); + UserDto userDto = userDao.selectByLogin(dbSession, USER_LOGIN); + assertThat(userDto).isNotNull(); + assertThat(userDto.isActive()).isTrue(); + assertThat(userDto.getName()).isEqualTo("John"); + assertThat(userDto.getEmail()).isEqualTo("john@email.com"); + assertThat(userDto.getExternalIdentity()).isEqualTo("johndoo"); + assertThat(userDto.getExternalIdentityProvider()).isEqualTo("github"); - assertThat(newUser.login()).isEqualTo(USER_LOGIN); - assertThat(newUser.name()).isEqualTo("John"); - assertThat(newUser.email()).isEqualTo("john@email.com"); - assertThat(newUser.externalIdentity().getProvider()).isEqualTo("github"); - assertThat(newUser.externalIdentity().getId()).isEqualTo("johndoo"); + verifyUserGroups(USER_LOGIN, DEFAULT_GROUP); + } + + @Test + public void authenticate_new_user_with_groups() throws Exception { + addGroup("group1"); + addGroup("group2"); + + underTest.authenticate(UserIdentity.builder() + .setProviderLogin("johndoo") + .setLogin(USER_LOGIN) + .setName("John") + // group3 doesn't exist in db, it will be ignored + .setGroups(newHashSet("group1", "group2", "group3")) + .build(), IDENTITY_PROVIDER, httpSession); + dbSession.commit(); + + UserDto userDto = userDao.selectByLogin(dbSession, USER_LOGIN); + assertThat(userDto).isNotNull(); + + verifyUserGroups(USER_LOGIN, "group1", "group2"); } @Test public void authenticate_existing_user() throws Exception { - when(userDao.selectByLogin(dbSession, USER_IDENTITY.getLogin())).thenReturn(ACTIVE_USER); + userDao.insert(dbSession, new UserDto() + .setLogin(USER_LOGIN) + .setActive(true) + .setName("Old name") + .setEmail("Old email") + .setExternalIdentity("old identity") + .setExternalIdentityProvider("old provide") + ); + dbSession.commit(); underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, httpSession); + dbSession.commit(); - ArgumentCaptor updateUserArgumentCaptor = ArgumentCaptor.forClass(UpdateUser.class); - verify(userUpdater).update(eq(dbSession), updateUserArgumentCaptor.capture()); - UpdateUser updateUser = updateUserArgumentCaptor.getValue(); - - assertThat(updateUser.login()).isEqualTo(USER_LOGIN); - assertThat(updateUser.name()).isEqualTo("John"); - assertThat(updateUser.email()).isEqualTo("john@email.com"); - assertThat(updateUser.externalIdentity().getProvider()).isEqualTo("github"); - assertThat(updateUser.externalIdentity().getId()).isEqualTo("johndoo"); - assertThat(updateUser.isPasswordChanged()).isTrue(); - assertThat(updateUser.password()).isNull(); + UserDto userDto = userDao.selectByLogin(dbSession, USER_LOGIN); + assertThat(userDto).isNotNull(); + assertThat(userDto.isActive()).isTrue(); + assertThat(userDto.getName()).isEqualTo("John"); + assertThat(userDto.getEmail()).isEqualTo("john@email.com"); + assertThat(userDto.getExternalIdentity()).isEqualTo("johndoo"); + assertThat(userDto.getExternalIdentityProvider()).isEqualTo("github"); } @Test public void authenticate_existing_disabled_user() throws Exception { - when(userDao.selectByLogin(dbSession, USER_IDENTITY.getLogin())).thenReturn(UNACTIVE_USER); - when(userDao.selectOrFailByLogin(dbSession, USER_IDENTITY.getLogin())).thenReturn(UNACTIVE_USER); + userDao.insert(dbSession, new UserDto() + .setLogin(USER_LOGIN) + .setActive(false) + .setName("Old name") + .setEmail("Old email") + .setExternalIdentity("old identity") + .setExternalIdentityProvider("old provide") + ); + dbSession.commit(); underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, httpSession); + dbSession.commit(); - ArgumentCaptor newUserArgumentCaptor = ArgumentCaptor.forClass(NewUser.class); - verify(userUpdater).create(eq(dbSession), newUserArgumentCaptor.capture()); + UserDto userDto = userDao.selectByLogin(dbSession, USER_LOGIN); + assertThat(userDto).isNotNull(); + assertThat(userDto.isActive()).isTrue(); + assertThat(userDto.getName()).isEqualTo("John"); + assertThat(userDto.getEmail()).isEqualTo("john@email.com"); + assertThat(userDto.getExternalIdentity()).isEqualTo("johndoo"); + assertThat(userDto.getExternalIdentityProvider()).isEqualTo("github"); + } + + @Test + public void authenticate_existing_user_and_add_new_groups() throws Exception { + userDao.insert(dbSession, new UserDto() + .setLogin(USER_LOGIN) + .setActive(true) + .setName("John") + ); + addGroup("group1"); + addGroup("group2"); + dbSession.commit(); + + underTest.authenticate(UserIdentity.builder() + .setProviderLogin("johndoo") + .setLogin(USER_LOGIN) + .setName("John") + // group3 doesn't exist in db, it will be ignored + .setGroups(newHashSet("group1", "group2", "group3")) + .build(), IDENTITY_PROVIDER, httpSession); + dbSession.commit(); + + Set userGroups = new HashSet<>(dbClient.groupMembershipDao().selectGroupsByLogins(dbSession, singletonList(USER_LOGIN)).get(USER_LOGIN)); + assertThat(userGroups).containsOnly("group1", "group2"); + } + + @Test + public void authenticate_existing_user_and_remove_groups() throws Exception { + UserDto user = new UserDto() + .setLogin(USER_LOGIN) + .setActive(true) + .setName("John"); + userDao.insert(dbSession, user); + + GroupDto group1 = addGroup("group1"); + GroupDto group2 = addGroup("group2"); + dbClient.userGroupDao().insert(dbSession, new UserGroupDto().setUserId(user.getId()).setGroupId(group1.getId())); + dbClient.userGroupDao().insert(dbSession, new UserGroupDto().setUserId(user.getId()).setGroupId(group2.getId())); + dbSession.commit(); + + Set userGroups = new HashSet<>(dbClient.groupMembershipDao().selectGroupsByLogins(dbSession, singletonList(USER_LOGIN)).get(USER_LOGIN)); + assertThat(userGroups).containsOnly("group1", "group2"); + + underTest.authenticate(UserIdentity.builder() + .setProviderLogin("johndoo") + .setLogin(USER_LOGIN) + .setName("John") + // Only group1 is returned by the id provider => group2 will be removed + .setGroups(newHashSet("group1")) + .build(), IDENTITY_PROVIDER, httpSession); + dbSession.commit(); + + verifyUserGroups(USER_LOGIN, "group1"); + } + + @Test + public void authenticate_existing_user_and_remove_all_groups() throws Exception { + UserDto user = new UserDto() + .setLogin(USER_LOGIN) + .setActive(true) + .setName("John"); + userDao.insert(dbSession, user); + + GroupDto group1 = addGroup("group1"); + GroupDto group2 = addGroup("group2"); + dbClient.userGroupDao().insert(dbSession, new UserGroupDto().setUserId(user.getId()).setGroupId(group1.getId())); + dbClient.userGroupDao().insert(dbSession, new UserGroupDto().setUserId(user.getId()).setGroupId(group2.getId())); + dbSession.commit(); + + Set userGroups = new HashSet<>(dbClient.groupMembershipDao().selectGroupsByLogins(dbSession, singletonList(USER_LOGIN)).get(USER_LOGIN)); + assertThat(userGroups).containsOnly("group1", "group2"); + + underTest.authenticate(UserIdentity.builder() + .setProviderLogin("johndoo") + .setLogin(USER_LOGIN) + .setName("John") + // No group => group1 and group2 will be removed + .setGroups(Collections.emptySet()) + .build(), IDENTITY_PROVIDER, httpSession); + dbSession.commit(); + + verifyNoUserGroups(USER_LOGIN); } @Test public void update_session_for_rails() throws Exception { - when(userDao.selectByLogin(dbSession, USER_IDENTITY.getLogin())).thenReturn(ACTIVE_USER); + UserDto userDto = UserTesting.newUserDto().setLogin(USER_LOGIN); + userDao.insert(dbSession, userDto); + dbSession.commit(); underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, httpSession); - verify(httpSession).setAttribute("user_id", ACTIVE_USER.getId()); + verify(httpSession).setAttribute("user_id", userDto.getId()); } @Test public void fail_to_authenticate_new_user_when_allow_users_to_signup_is_false() throws Exception { - when(userDao.selectByLogin(dbSession, USER_IDENTITY.getLogin())).thenReturn(null); - when(userDao.selectOrFailByLogin(dbSession, USER_IDENTITY.getLogin())).thenReturn(ACTIVE_USER); - TestIdentityProvider identityProvider = new TestIdentityProvider() .setKey("github") .setName("Github") @@ -152,12 +291,33 @@ public class UserIdentityAuthenticatorTest { @Test public void fail_to_authenticate_new_user_when_email_already_exists() throws Exception { - when(userDao.selectByLogin(dbSession, USER_IDENTITY.getLogin())).thenReturn(null); - when(userDao.selectOrFailByLogin(dbSession, USER_IDENTITY.getLogin())).thenReturn(ACTIVE_USER); - when(userDao.doesEmailExist(dbSession, USER_IDENTITY.getEmail())).thenReturn(true); + UserDto userDto = UserTesting.newUserDto() + .setLogin("Existing user with same email") + .setActive(true) + .setEmail("john@email.com"); + userDao.insert(dbSession, userDto); + dbSession.commit(); thrown.expect(UnauthorizedException.class); - thrown.expectMessage("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."); + thrown.expectMessage("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."); underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, httpSession); } + + private void verifyUserGroups(String userLogin, String... groups) { + Set userGroups = new HashSet<>(dbClient.groupMembershipDao().selectGroupsByLogins(dbSession, singletonList(USER_LOGIN)).get(userLogin)); + assertThat(userGroups).containsOnly(groups); + } + + private void verifyNoUserGroups(String userLogin) { + Set userGroups = new HashSet<>(dbClient.groupMembershipDao().selectGroupsByLogins(dbSession, singletonList(USER_LOGIN)).get(userLogin)); + assertThat(userGroups).isEmpty(); + } + + private GroupDto addGroup(String name) { + GroupDto group = new GroupDto().setName(name); + groupDao.insert(dbSession, group); + dbSession.commit(); + return group; + } } diff --git a/sonar-db/src/main/java/org/sonar/db/user/GroupDao.java b/sonar-db/src/main/java/org/sonar/db/user/GroupDao.java index db5e428cc14..805526b9cef 100644 --- a/sonar-db/src/main/java/org/sonar/db/user/GroupDao.java +++ b/sonar-db/src/main/java/org/sonar/db/user/GroupDao.java @@ -19,10 +19,13 @@ */ package org.sonar.db.user; +import com.google.common.base.Function; +import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Locale; import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.apache.commons.lang.StringUtils; import org.apache.ibatis.session.RowBounds; @@ -33,6 +36,8 @@ import org.sonar.db.DbSession; import org.sonar.db.RowNotFoundException; import org.sonar.db.WildcardPosition; +import static org.sonar.db.DatabaseUtils.executeLargeInputs; + public class GroupDao implements Dao { private System2 system; @@ -54,6 +59,10 @@ public class GroupDao implements Dao { return mapper(session).selectByKey(key); } + public List selectByNames(DbSession session, Collection names) { + return executeLargeInputs(names, new SelectByNames(mapper(session))); + } + public GroupDto selectOrFailById(DbSession dbSession, long groupId) { GroupDto group = selectById(dbSession, groupId); if (group == null) { @@ -110,4 +119,17 @@ public class GroupDao implements Dao { private GroupMapper mapper(DbSession session) { return session.getMapper(GroupMapper.class); } + + private static class SelectByNames implements Function, List> { + private final GroupMapper mapper; + + private SelectByNames(GroupMapper mapper) { + this.mapper = mapper; + } + + @Override + public List apply(@Nonnull List partitionOfNames) { + return mapper.selectByNames(partitionOfNames); + } + } } diff --git a/sonar-db/src/main/java/org/sonar/db/user/GroupMapper.java b/sonar-db/src/main/java/org/sonar/db/user/GroupMapper.java index a5bb52cf5fb..4742a1f667f 100644 --- a/sonar-db/src/main/java/org/sonar/db/user/GroupMapper.java +++ b/sonar-db/src/main/java/org/sonar/db/user/GroupMapper.java @@ -34,6 +34,8 @@ public interface GroupMapper { List selectByUserLogin(String userLogin); + List selectByNames(@Param("names") List names); + void insert(GroupDto groupDto); void update(GroupDto item); diff --git a/sonar-db/src/main/resources/org/sonar/db/user/GroupMapper.xml b/sonar-db/src/main/resources/org/sonar/db/user/GroupMapper.xml index 826828fcc9b..650e144d630 100644 --- a/sonar-db/src/main/resources/org/sonar/db/user/GroupMapper.xml +++ b/sonar-db/src/main/resources/org/sonar/db/user/GroupMapper.xml @@ -48,6 +48,18 @@ + + INSERT INTO groups (name, description, created_at, updated_at) VALUES (#{name}, #{description}, #{createdAt}, #{updatedAt}) diff --git a/sonar-db/src/test/java/org/sonar/db/user/GroupDaoTest.java b/sonar-db/src/test/java/org/sonar/db/user/GroupDaoTest.java index cf6fedb69fb..153aaefc849 100644 --- a/sonar-db/src/test/java/org/sonar/db/user/GroupDaoTest.java +++ b/sonar-db/src/test/java/org/sonar/db/user/GroupDaoTest.java @@ -19,6 +19,7 @@ */ package org.sonar.db.user; +import java.util.Collections; import java.util.List; import org.junit.Rule; import org.junit.Test; @@ -27,6 +28,8 @@ import org.sonar.api.utils.System2; import org.sonar.db.DbSession; import org.sonar.db.DbTester; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -79,6 +82,19 @@ public class GroupDaoTest { assertThat(underTest.selectByUserLogin(dbSession, "max")).isEmpty(); } + @Test + public void select_by_names() { + underTest.insert(dbSession, new GroupDto().setName("group1")); + underTest.insert(dbSession, new GroupDto().setName("group2")); + underTest.insert(dbSession, new GroupDto().setName("group3")); + dbSession.commit(); + + assertThat(underTest.selectByNames(dbSession, asList("group1", "group2", "group3"))).hasSize(3); + assertThat(underTest.selectByNames(dbSession, singletonList("group1"))).hasSize(1); + assertThat(underTest.selectByNames(dbSession, asList("group1", "unknown"))).hasSize(1); + assertThat(underTest.selectByNames(dbSession, Collections.emptyList())).isEmpty(); + } + @Test public void insert() { when(system2.now()).thenReturn(DateUtils.parseDate("2014-09-08").getTime()); diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/server/authentication/UserIdentity.java b/sonar-plugin-api/src/main/java/org/sonar/api/server/authentication/UserIdentity.java index 80bba8904f7..119cb2b32a0 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/server/authentication/UserIdentity.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/server/authentication/UserIdentity.java @@ -19,12 +19,20 @@ */ package org.sonar.api.server.authentication; +import com.google.common.base.Predicate; +import java.util.HashSet; +import java.util.Set; import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; +import org.sonar.api.CoreProperties; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.FluentIterable.from; import static org.apache.commons.lang.StringUtils.isNotBlank; +import static org.sonar.api.user.UserGroupValidation.validateGroupName; /** * User information provided by the Identity Provider to be register into the platform. @@ -38,12 +46,16 @@ public final class UserIdentity { private final String login; private final String name; private final String email; + private final boolean groupsProvided; + private final Set groups; private UserIdentity(Builder builder) { this.providerLogin = builder.providerLogin; this.login = builder.login; this.name = builder.name; this.email = builder.email; + this.groupsProvided = builder.groupsProvided; + this.groups = builder.groups; } /** @@ -78,6 +90,24 @@ public final class UserIdentity { return email; } + /** + * Return true if groups should be synchronized for this user. + * + * @since 5.5 + */ + public boolean shouldSyncGroups() { + return groupsProvided; + } + + /** + * List of group membership of the user. Only existing groups in SonarQube will be synchronized. + * + * @since 5.5 + */ + public Set getGroups() { + return groups; + } + public static Builder builder() { return new Builder(); } @@ -87,6 +117,8 @@ public final class UserIdentity { private String login; private String name; private String email; + private boolean groupsProvided = false; + private Set groups = new HashSet<>(); private Builder() { } @@ -123,6 +155,29 @@ public final class UserIdentity { return this; } + /** + * Set group membership of the user. This method should only be used when synchronization of groups should be done. + *
    + *
  • When groups are not empty, group membership is synchronized when user logs in : + *
      + *
    • User won't belong anymore to a group that is not in the list (even the default group defined in {@link CoreProperties.CORE_DEFAULT_GROUP})
    • + *
    • User will belong only to groups that exist in SonarQube
    • + *
    • Groups that don't exist in SonarQube are silently ignored
    • + *
    + *
  • When groups are empty, user won't belong to any group
  • + *
+ * + * @throws NullPointerException when groups is null + * @since 5.5 + */ + public Builder setGroups(Set groups) { + checkNotNull(groups, "Groups cannot be null, please don't this method if groups should not be synchronized."); + from(groups).filter(ValidateGroupName.INSTANCE).toList(); + this.groupsProvided = true; + this.groups = groups; + return this; + } + public UserIdentity build() { validateProviderLogin(providerLogin); validateLogin(login); @@ -131,23 +186,33 @@ public final class UserIdentity { return new UserIdentity(this); } - private static void validateProviderLogin(String providerLogin){ + private static void validateProviderLogin(String providerLogin) { checkArgument(isNotBlank(providerLogin), "Provider login must not be blank"); checkArgument(providerLogin.length() <= 255, "Provider login size is incorrect (maximum 255 characters)"); } - private static void validateLogin(String login){ + private static void validateLogin(String login) { checkArgument(isNotBlank(login), "User login must not be blank"); checkArgument(login.length() <= 255 && login.length() >= 3, "User login size is incorrect (Between 3 and 255 characters)"); } - private static void validateName(String name){ + private static void validateName(String name) { checkArgument(isNotBlank(name), "User name must not be blank"); checkArgument(name.length() <= 200, "User name size is too big (200 characters max)"); } - private static void validateEmail(@Nullable String email){ + private static void validateEmail(@Nullable String email) { checkArgument(email == null || email.length() <= 100, "User email size is too big (100 characters max)"); } } + + private enum ValidateGroupName implements Predicate { + INSTANCE; + + @Override + public boolean apply(@Nonnull String input) { + validateGroupName(input); + return true; + } + } } diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/user/UserGroupValidation.java b/sonar-plugin-api/src/main/java/org/sonar/api/user/UserGroupValidation.java new file mode 100644 index 00000000000..98c2d594c0f --- /dev/null +++ b/sonar-plugin-api/src/main/java/org/sonar/api/user/UserGroupValidation.java @@ -0,0 +1,40 @@ +/* + * 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.api.user; + +import org.sonar.api.security.DefaultGroups; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Strings.isNullOrEmpty; + +public class UserGroupValidation { + + private static final int GROUP_NAME_MAX_LENGTH = 255; + + private UserGroupValidation() { + // Only static methods + } + + public static void validateGroupName(String groupName) { + checkArgument(!isNullOrEmpty(groupName) && groupName.trim().length() > 0, "Group cannot contain empty group name"); + checkArgument(groupName.length() <= GROUP_NAME_MAX_LENGTH, "Group name cannot be longer than %s characters", GROUP_NAME_MAX_LENGTH); + checkArgument(!DefaultGroups.isAnyone(groupName), "Anyone group cannot be used"); + } +} diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/server/authentication/UserIdentityTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/server/authentication/UserIdentityTest.java index 25096e6cebe..3ef0c9f9bbd 100644 --- a/sonar-plugin-api/src/test/java/org/sonar/api/server/authentication/UserIdentityTest.java +++ b/sonar-plugin-api/src/test/java/org/sonar/api/server/authentication/UserIdentityTest.java @@ -24,6 +24,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import static com.google.common.collect.Sets.newHashSet; import static org.assertj.core.api.Assertions.assertThat; public class UserIdentityTest { @@ -44,6 +45,8 @@ public class UserIdentityTest { assertThat(underTest.getLogin()).isEqualTo("1234"); assertThat(underTest.getName()).isEqualTo("John"); assertThat(underTest.getEmail()).isEqualTo("john@email.com"); + assertThat(underTest.shouldSyncGroups()).isFalse(); + assertThat(underTest.getGroups()).isEmpty(); } @Test @@ -174,4 +177,97 @@ public class UserIdentityTest { .setEmail(Strings.repeat("1", 101)) .build(); } + + @Test + public void create_user_with_groups() throws Exception { + UserIdentity underTest = UserIdentity.builder() + .setProviderLogin("john") + .setLogin("1234") + .setName("John") + .setEmail("john@email.com") + .setGroups(newHashSet("admin", "user")) + .build(); + + assertThat(underTest.shouldSyncGroups()).isTrue(); + assertThat(underTest.getGroups()).containsOnly("admin", "user"); + } + + @Test + public void fail_when_groups_are_null() throws Exception { + thrown.expect(NullPointerException.class); + thrown.expectMessage("Groups cannot be null, please don't this method if groups should not be synchronized."); + + UserIdentity.builder() + .setProviderLogin("john") + .setLogin("1234") + .setName("John") + .setEmail("john@email.com") + .setGroups(null); + } + + @Test + public void fail_when_groups_contain_empty_group_name() throws Exception { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Group cannot contain empty group name"); + + UserIdentity.builder() + .setProviderLogin("john") + .setLogin("1234") + .setName("John") + .setEmail("john@email.com") + .setGroups(newHashSet("")); + } + + @Test + public void fail_when_groups_contain_only_blank_space() throws Exception { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Group cannot contain empty group name"); + + UserIdentity.builder() + .setProviderLogin("john") + .setLogin("1234") + .setName("John") + .setEmail("john@email.com") + .setGroups(newHashSet(" ")); + } + + @Test + public void fail_when_groups_contain_null_group_name() throws Exception { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Group cannot contain empty group name"); + + UserIdentity.builder() + .setProviderLogin("john") + .setLogin("1234") + .setName("John") + .setEmail("john@email.com") + .setGroups(newHashSet((String)null)); + } + + @Test + public void fail_when_groups_contain_anyone() throws Exception { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Anyone group cannot be used"); + + UserIdentity.builder() + .setProviderLogin("john") + .setLogin("1234") + .setName("John") + .setEmail("john@email.com") + .setGroups(newHashSet("Anyone")); + } + + @Test + public void fail_when_groups_contain_too_long_group_name() throws Exception { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Group name cannot be longer than 255 characters"); + + UserIdentity.builder() + .setProviderLogin("john") + .setLogin("1234") + .setName("John") + .setEmail("john@email.com") + .setGroups(newHashSet(Strings.repeat("group", 300))); + } + } diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/user/UserGroupValidationTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/user/UserGroupValidationTest.java new file mode 100644 index 00000000000..e952c7df7a9 --- /dev/null +++ b/sonar-plugin-api/src/test/java/org/sonar/api/user/UserGroupValidationTest.java @@ -0,0 +1,71 @@ +/* + * 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.api.user; + +import com.google.common.base.Strings; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class UserGroupValidationTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void fail_when_group_name_is_Anyone() throws Exception { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Anyone group cannot be used"); + + UserGroupValidation.validateGroupName("AnyOne"); + } + + @Test + public void fail_when_group_name_is_empty() throws Exception { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Group cannot contain empty group name"); + + UserGroupValidation.validateGroupName(""); + } + + @Test + public void fail_when_group_name_contains_only_blank() throws Exception { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Group cannot contain empty group name"); + + UserGroupValidation.validateGroupName(" "); + } + + @Test + public void fail_when_group_name_is_too_big() throws Exception { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Group name cannot be longer than 255 characters"); + + UserGroupValidation.validateGroupName(Strings.repeat("name", 300)); + } + + @Test + public void fail_when_group_name_is_null() throws Exception { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Group cannot contain empty group name"); + + UserGroupValidation.validateGroupName(null); + } +} -- 2.39.5