aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAntoine Vigneau <antoine.vigneau@sonarsource.com>2023-05-02 17:22:50 +0200
committersonartech <sonartech@sonarsource.com>2023-05-11 20:03:14 +0000
commit6f601b7241ece842321d2c5cd1e1a75b28e2f850 (patch)
tree474d15e7ec47295cec6ac203ed45976e039414e2
parent8643251e97d9975814a28a348e76b4a3f8d38ec9 (diff)
downloadsonarqube-6f601b7241ece842321d2c5cd1e1a75b28e2f850.tar.gz
sonarqube-6f601b7241ece842321d2c5cd1e1a75b28e2f850.zip
SONAR-19085 Create and update GitHub groups
-rw-r--r--server/sonar-db-dao/src/it/java/org/sonar/db/user/ExternalGroupDaoIT.java41
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/user/ExternalGroupDao.java9
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/user/ExternalGroupMapper.java7
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/user/GroupQuery.java2
-rw-r--r--server/sonar-db-dao/src/main/resources/org/sonar/db/user/ExternalGroupMapper.xml25
-rw-r--r--server/sonar-db-dao/src/testFixtures/java/org/sonar/db/user/UserDbTester.java14
-rw-r--r--server/sonar-webserver-webapi/src/it/java/org/sonar/server/usergroups/ws/ExternalGroupServiceIT.java94
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/ExternalGroupService.java71
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/GroupRegistration.java23
9 files changed, 277 insertions, 9 deletions
diff --git a/server/sonar-db-dao/src/it/java/org/sonar/db/user/ExternalGroupDaoIT.java b/server/sonar-db-dao/src/it/java/org/sonar/db/user/ExternalGroupDaoIT.java
index 26d47127977..6de7ba2c279 100644
--- a/server/sonar-db-dao/src/it/java/org/sonar/db/user/ExternalGroupDaoIT.java
+++ b/server/sonar-db-dao/src/it/java/org/sonar/db/user/ExternalGroupDaoIT.java
@@ -21,6 +21,7 @@ package org.sonar.db.user;
import java.util.ArrayList;
import java.util.List;
+import java.util.Optional;
import org.junit.Rule;
import org.junit.Test;
import org.sonar.db.DbSession;
@@ -30,6 +31,10 @@ import static org.assertj.core.api.Assertions.assertThat;
public class ExternalGroupDaoIT {
+ private static final String GROUP_UUID = "uuid";
+ private static final String EXTERNAL_ID = "external_id";
+ private static final String EXTERNAL_IDENTITY_PROVIDER = "external_identity_provider";
+
@Rule
public final DbTester db = DbTester.create();
@@ -41,17 +46,28 @@ public class ExternalGroupDaoIT {
@Test
public void insert_savesExternalGroup() {
- GroupDto localGroup = insertGroup("12345");
+ GroupDto localGroup = insertGroup(GROUP_UUID);
insertGroup("67689");
- ExternalGroupDto externalGroupDto = externalGroup("12345", "providerId");
+ ExternalGroupDto externalGroupDto = externalGroup(GROUP_UUID, EXTERNAL_IDENTITY_PROVIDER);
underTest.insert(dbSession, externalGroupDto);
- List<ExternalGroupDto> savedGroups = underTest.selectByIdentityProvider(dbSession, "providerId");
+ List<ExternalGroupDto> savedGroups = underTest.selectByIdentityProvider(dbSession, EXTERNAL_IDENTITY_PROVIDER);
assertThat(savedGroups)
.hasSize(1)
.contains(createExternalGroupDto(localGroup.getName(), externalGroupDto));
}
@Test
+ public void selectByGroupUuid_shouldReturnExternalGroup() {
+ ExternalGroupDto expectedExternalGroupDto = new ExternalGroupDto(GROUP_UUID, EXTERNAL_ID, EXTERNAL_IDENTITY_PROVIDER);
+ underTest.insert(dbSession, expectedExternalGroupDto);
+
+ Optional<ExternalGroupDto> actualExternalGroupDto = underTest.selectByGroupUuid(dbSession, GROUP_UUID);
+
+ assertThat(actualExternalGroupDto).isPresent();
+ compareExpectedAndActualExternalGroupDto(expectedExternalGroupDto, actualExternalGroupDto.get());
+ }
+
+ @Test
public void selectByIdentityProvider_returnOnlyGroupForTheIdentityProvider() {
List<ExternalGroupDto> expectedGroups = createAndInsertExternalGroupDtos("provider1", 3);
createAndInsertExternalGroupDtos("provider2", 1);
@@ -60,6 +76,25 @@ public class ExternalGroupDaoIT {
}
@Test
+ public void selectByExternalIdAndIdentityProvider_shouldReturnOnlyMatchingExternalGroup() {
+ ExternalGroupDto expectedExternalGroupDto = new ExternalGroupDto(GROUP_UUID, EXTERNAL_ID, EXTERNAL_IDENTITY_PROVIDER);
+ underTest.insert(dbSession, expectedExternalGroupDto);
+ underTest.insert(dbSession, new ExternalGroupDto(GROUP_UUID + "1", EXTERNAL_ID, "another_external_identity_provider"));
+ underTest.insert(dbSession, new ExternalGroupDto(GROUP_UUID + "2", "another_external_id", EXTERNAL_IDENTITY_PROVIDER));
+ underTest.insert(dbSession, new ExternalGroupDto(GROUP_UUID + "3", "whatever", "whatever"));
+
+ Optional<ExternalGroupDto> actualExternalGroupDto = underTest.selectByExternalIdAndIdentityProvider(dbSession, EXTERNAL_ID, EXTERNAL_IDENTITY_PROVIDER);
+
+ compareExpectedAndActualExternalGroupDto(expectedExternalGroupDto, actualExternalGroupDto.get());
+ }
+
+ private void compareExpectedAndActualExternalGroupDto(ExternalGroupDto expectedExternalGroupDto, ExternalGroupDto actualExternalGroupDto) {
+ assertThat(actualExternalGroupDto)
+ .usingRecursiveComparison()
+ .isEqualTo(expectedExternalGroupDto);
+ }
+
+ @Test
public void deleteByGroupUuid_deletesTheGroup() {
List<ExternalGroupDto> insertedGroups = createAndInsertExternalGroupDtos("provider1", 3);
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/user/ExternalGroupDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/user/ExternalGroupDao.java
index 9980aaa60ce..743236638fb 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/user/ExternalGroupDao.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/user/ExternalGroupDao.java
@@ -20,6 +20,7 @@
package org.sonar.db.user;
import java.util.List;
+import java.util.Optional;
import org.sonar.db.Dao;
import org.sonar.db.DbSession;
@@ -29,6 +30,10 @@ public class ExternalGroupDao implements Dao {
mapper(dbSession).insert(externalGroupDto);
}
+ public Optional<ExternalGroupDto> selectByGroupUuid(DbSession dbSession, String groupUuid) {
+ return mapper(dbSession).selectByGroupUuid(groupUuid);
+ }
+
public List<ExternalGroupDto> selectByIdentityProvider(DbSession dbSession, String identityProvider) {
return mapper(dbSession).selectByIdentityProvider(identityProvider);
}
@@ -37,6 +42,10 @@ public class ExternalGroupDao implements Dao {
mapper(dbSession).deleteByGroupUuid(groupUuid);
}
+ public Optional<ExternalGroupDto> selectByExternalIdAndIdentityProvider(DbSession dbSession, String externalId, String identityProvider) {
+ return mapper(dbSession).selectByExternalIdAndIdentityProvider(externalId, identityProvider);
+ }
+
private static ExternalGroupMapper mapper(DbSession session) {
return session.getMapper(ExternalGroupMapper.class);
}
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/user/ExternalGroupMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/user/ExternalGroupMapper.java
index b456be15a66..908aae7664f 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/user/ExternalGroupMapper.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/user/ExternalGroupMapper.java
@@ -20,15 +20,18 @@
package org.sonar.db.user;
import java.util.List;
+import java.util.Optional;
import org.apache.ibatis.annotations.Param;
public interface ExternalGroupMapper {
void insert(ExternalGroupDto externalGroupDto);
- List<ExternalGroupDto> selectByIdentityProvider(String identityProvider);
+ Optional<ExternalGroupDto> selectByGroupUuid(@Param("groupUuid") String groupUuid);
- void deleteByGroupUuid(@Param("groupUuid") String groupUuid);
+ List<ExternalGroupDto> selectByIdentityProvider(@Param("identityProvider") String identityProvider);
+ Optional<ExternalGroupDto> selectByExternalIdAndIdentityProvider(@Param("externalId") String externalId, @Param("identityProvider") String identityProvider);
+ void deleteByGroupUuid(@Param("groupUuid") String groupUuid);
}
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/user/GroupQuery.java b/server/sonar-db-dao/src/main/java/org/sonar/db/user/GroupQuery.java
index a8cf01456dc..d169534193b 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/user/GroupQuery.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/user/GroupQuery.java
@@ -30,7 +30,7 @@ public class GroupQuery {
private final String searchText;
private final String isManagedSqlClause;
- private GroupQuery(@Nullable String searchText, @Nullable String isManagedSqlClause) {
+ GroupQuery(@Nullable String searchText, @Nullable String isManagedSqlClause) {
this.searchText = searchTextToSearchTextSql(searchText);
this.isManagedSqlClause = isManagedSqlClause;
}
diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/user/ExternalGroupMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/user/ExternalGroupMapper.xml
index be2db7475b4..a63497ece33 100644
--- a/server/sonar-db-dao/src/main/resources/org/sonar/db/user/ExternalGroupMapper.xml
+++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/user/ExternalGroupMapper.xml
@@ -4,6 +4,12 @@
<mapper namespace="org.sonar.db.user.ExternalGroupMapper">
+ <sql id="externalGroupColumns">
+ eg.group_uuid as groupUuid,
+ eg.external_group_id as external_id,
+ eg.external_identity_provider as externalIdentityProvider
+ </sql>
+
<insert id="insert" useGeneratedKeys="false" parameterType="ExternalGroup">
insert into external_groups (
group_uuid,
@@ -16,17 +22,30 @@
)
</insert>
+ <select id="selectByGroupUuid" parameterType="String" resultType="ExternalGroup">
+ SELECT
+ <include refid="externalGroupColumns"/>
+ FROM external_groups eg
+ WHERE eg.group_uuid=#{groupUuid,jdbcType=VARCHAR}
+ </select>
+
<select id="selectByIdentityProvider" parameterType="String" resultType="ExternalGroup">
SELECT
- eg.group_uuid as groupUuid,
- eg.external_group_id as external_id,
- eg.external_identity_provider as externalIdentityProvider,
+ <include refid="externalGroupColumns"/>,
g.name as name
FROM external_groups eg
LEFT JOIN groups g ON eg.group_uuid = g.uuid
WHERE eg.external_identity_provider=#{identityProvider,jdbcType=VARCHAR}
</select>
+ <select id="selectByExternalIdAndIdentityProvider" parameterType="String" resultType="ExternalGroup">
+ SELECT
+ <include refid="externalGroupColumns"/>
+ FROM external_groups eg
+ WHERE eg.external_group_id=#{externalId,jdbcType=VARCHAR}
+ AND eg.external_identity_provider=#{identityProvider,jdbcType=VARCHAR}
+ </select>
+
<delete id="deleteByGroupUuid" parameterType="String">
delete from external_groups where group_uuid = #{groupUuid, jdbcType=VARCHAR}
</delete>
diff --git a/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/user/UserDbTester.java b/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/user/UserDbTester.java
index 45a2815630b..4513d2608e3 100644
--- a/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/user/UserDbTester.java
+++ b/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/user/UserDbTester.java
@@ -163,6 +163,12 @@ public class UserDbTester {
db.commit();
}
+ public ExternalGroupDto insertExternalGroup(ExternalGroupDto dto) {
+ db.getDbClient().externalGroupDao().insert(db.getSession(), dto);
+ db.commit();
+ return dto;
+ }
+
public ScimGroupDto insertScimGroup(GroupDto dto) {
ScimGroupDto result = db.getDbClient().scimGroupDao().enableScimForGroup(db.getSession(), dto.getUuid());
db.commit();
@@ -184,6 +190,14 @@ public class UserDbTester {
return db.getDbClient().groupDao().selectByName(db.getSession(), name);
}
+ public int countAllGroups() {
+ return db.getDbClient().groupDao().countByQuery(db.getSession(), new GroupQuery(null, null));
+ }
+
+ public Optional<ExternalGroupDto> selectExternalGroupByGroupUuid(String groupUuid) {
+ return db.getDbClient().externalGroupDao().selectByGroupUuid(db.getSession(), groupUuid);
+ }
+
// GROUP MEMBERSHIP
public UserGroupDto insertMember(GroupDto group, UserDto user) {
diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/usergroups/ws/ExternalGroupServiceIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/usergroups/ws/ExternalGroupServiceIT.java
new file mode 100644
index 00000000000..a355dbfd164
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/usergroups/ws/ExternalGroupServiceIT.java
@@ -0,0 +1,94 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.usergroups.ws;
+
+import java.util.Optional;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.utils.System2;
+import org.sonar.core.util.UuidFactoryFast;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.user.ExternalGroupDto;
+import org.sonar.db.user.GroupDto;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ExternalGroupServiceIT {
+
+ private static final String GROUP_NAME = "GROUP_NAME";
+ private static final String EXTERNAL_ID = "EXTERNAL_ID";
+ private static final String EXTERNAL_IDENTITY_PROVIDER = "EXTERNAL_IDENTITY_PROVIDER";
+
+ @Rule
+ public DbTester dbTester = DbTester.create(System2.INSTANCE);
+ private final DbClient dbClient = dbTester.getDbClient();
+ private final DbSession dbSession = dbTester.getSession();
+ private final GroupService groupService = new GroupService(dbClient, UuidFactoryFast.getInstance());
+
+ private final ExternalGroupService externalGroupService = new ExternalGroupService(dbClient, groupService);
+
+ @Test
+ public void createOrUpdateExternalGroup_whenNewGroup_shouldCreateIt() {
+ externalGroupService.createOrUpdateExternalGroup(dbSession, new GroupRegistration(EXTERNAL_ID, EXTERNAL_IDENTITY_PROVIDER, GROUP_NAME));
+
+ assertGroupAndExternalGroup();
+ }
+
+ @Test
+ public void createOrUpdateExternalGroup_whenExistingLocalGroup_shouldMatchAndMakeItExternal() {
+ dbTester.users().insertGroup(GROUP_NAME);
+
+ externalGroupService.createOrUpdateExternalGroup(dbSession, new GroupRegistration(EXTERNAL_ID, EXTERNAL_IDENTITY_PROVIDER, GROUP_NAME));
+
+ assertThat(dbTester.users().countAllGroups()).isEqualTo(1);
+ assertGroupAndExternalGroup();
+ }
+
+ @Test
+ public void createOrUpdateExternalGroup_whenExistingExternalGroup_shouldUpdate() {
+ dbTester.users().insertDefaultGroup();
+ GroupDto existingGroupDto = dbTester.users().insertGroup(GROUP_NAME);
+ dbTester.users().insertExternalGroup(new ExternalGroupDto(existingGroupDto.getUuid(), EXTERNAL_ID, EXTERNAL_IDENTITY_PROVIDER));
+
+ String updatedGroupName = "updated_" + GROUP_NAME;
+ externalGroupService.createOrUpdateExternalGroup(dbSession, new GroupRegistration(EXTERNAL_ID, EXTERNAL_IDENTITY_PROVIDER, updatedGroupName));
+
+ Optional<GroupDto> groupDto = dbTester.users().selectGroup(updatedGroupName);
+ assertThat(groupDto)
+ .isPresent().get()
+ .extracting(GroupDto::getName)
+ .isEqualTo(updatedGroupName);
+ }
+
+ private void assertGroupAndExternalGroup() {
+ Optional<GroupDto> groupDto = dbTester.users().selectGroup(GROUP_NAME);
+ assertThat(groupDto)
+ .isPresent().get()
+ .extracting(GroupDto::getName).isEqualTo(GROUP_NAME);
+
+ assertThat((dbTester.users().selectExternalGroupByGroupUuid(groupDto.get().getUuid())))
+ .isPresent().get()
+ .extracting(ExternalGroupDto::externalId, ExternalGroupDto::externalIdentityProvider)
+ .containsExactly(EXTERNAL_ID, EXTERNAL_IDENTITY_PROVIDER);
+ }
+
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/ExternalGroupService.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/ExternalGroupService.java
new file mode 100644
index 00000000000..65ead0b66d5
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/ExternalGroupService.java
@@ -0,0 +1,71 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.usergroups.ws;
+
+import java.util.Optional;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.user.ExternalGroupDto;
+import org.sonar.db.user.GroupDto;
+
+public class ExternalGroupService {
+
+ private final DbClient dbClient;
+ private final GroupService groupService;
+
+ public ExternalGroupService(DbClient dbClient, GroupService groupService) {
+ this.dbClient = dbClient;
+ this.groupService = groupService;
+ }
+
+ public void createOrUpdateExternalGroup(DbSession dbSession, GroupRegistration groupRegistration) {
+ Optional<GroupDto> groupDto = retrieveGroupByItsExternalInformation(dbSession, groupRegistration);
+ if (groupDto.isPresent()) {
+ updateExternalGroup(dbSession, groupDto.get(), groupRegistration.name());
+ } else {
+ createOrMatchExistingLocalGroup(dbSession, groupRegistration);
+ }
+ }
+
+ private Optional<GroupDto> retrieveGroupByItsExternalInformation(DbSession dbSession, GroupRegistration groupRegistration) {
+ Optional<ExternalGroupDto> externalGroupDto =
+ dbClient.externalGroupDao().selectByExternalIdAndIdentityProvider(dbSession, groupRegistration.externalId(), groupRegistration.externalIdentityProvider());
+ return externalGroupDto.flatMap(existingExternalGroupDto -> Optional.ofNullable(dbClient.groupDao().selectByUuid(dbSession, existingExternalGroupDto.groupUuid())));
+ }
+
+ private void updateExternalGroup(DbSession dbSession, GroupDto groupDto, String newName) {
+ groupService.updateGroup(dbSession, groupDto, newName);
+ }
+
+ private void createOrMatchExistingLocalGroup(DbSession dbSession, GroupRegistration groupRegistration) {
+ GroupDto groupDto = findOrCreateLocalGroup(dbSession, groupRegistration);
+ createExternalGroup(dbSession, groupDto.getUuid(), groupRegistration);
+ }
+
+ private GroupDto findOrCreateLocalGroup(DbSession dbSession, GroupRegistration groupRegistration) {
+ Optional<GroupDto> groupDto = groupService.findGroup(dbSession, groupRegistration.name());
+ return groupDto.orElseGet(() -> groupService.createGroup(dbSession, groupRegistration.name(), null));
+ }
+
+ private void createExternalGroup(DbSession dbSession, String groupUuid, GroupRegistration groupRegistration) {
+ dbClient.externalGroupDao().insert(dbSession, new ExternalGroupDto(groupUuid, groupRegistration.externalId(), groupRegistration.externalIdentityProvider()));
+ }
+
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/GroupRegistration.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/GroupRegistration.java
new file mode 100644
index 00000000000..cafa26e362b
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/GroupRegistration.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.usergroups.ws;
+
+public record GroupRegistration(String externalId, String externalIdentityProvider, String name) {
+}