From: Aurelien Poscia Date: Wed, 5 Jul 2023 09:27:38 +0000 (+0200) Subject: SONAR-19784 allow the CE to re-compute permissions X-Git-Tag: 10.2.0.77647~396 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=781f6976f9b44bec7d2a545f2407cabdfbf4c7a2;p=sonarqube.git SONAR-19784 allow the CE to re-compute permissions --- diff --git a/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java b/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java index 3ac0a968169..bff8ef8ec94 100644 --- a/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java +++ b/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java @@ -23,6 +23,7 @@ import com.google.common.annotations.VisibleForTesting; import java.time.Clock; import java.util.List; import javax.annotation.CheckForNull; +import org.slf4j.LoggerFactory; import org.sonar.api.SonarEdition; import org.sonar.api.SonarQubeSide; import org.sonar.api.config.EmailSettings; @@ -38,7 +39,6 @@ import org.sonar.api.utils.Durations; import org.sonar.api.utils.System2; import org.sonar.api.utils.UriReader; import org.sonar.api.utils.Version; -import org.slf4j.LoggerFactory; import org.sonar.ce.CeConfigurationModule; import org.sonar.ce.CeDistributedInformationImpl; import org.sonar.ce.CeHttpModule; @@ -115,6 +115,7 @@ import org.sonar.server.metric.UnanalyzedLanguageMetrics; import org.sonar.server.notification.DefaultNotificationManager; import org.sonar.server.notification.NotificationService; import org.sonar.server.notification.email.EmailNotificationChannel; +import org.sonar.server.permission.index.PermissionIndexer; import org.sonar.server.platform.DefaultNodeInformation; import org.sonar.server.platform.OfficialDistribution; import org.sonar.server.platform.ServerFileSystemImpl; @@ -387,6 +388,7 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer { QGChangeNotificationHandler.newMetadata(), ProjectMeasuresIndexer.class, EntityDefinitionIndexer.class, + PermissionIndexer.class, // views ViewIndexer.class, diff --git a/server/sonar-server-common/build.gradle b/server/sonar-server-common/build.gradle index 0ba7fb41ca6..64183060fd8 100644 --- a/server/sonar-server-common/build.gradle +++ b/server/sonar-server-common/build.gradle @@ -42,6 +42,8 @@ dependencies { testImplementation 'org.mockito:mockito-core' testImplementation 'org.sonarsource.api.plugin:sonar-plugin-api-test-fixtures' testImplementation testFixtures(project(':server:sonar-db-dao')) + testImplementation testFixtures(project(':server:sonar-webserver-auth')) + testImplementation testFixtures(project(':server:sonar-webserver-es')) testImplementation project(':sonar-plugin-api-impl') testImplementation project(':sonar-testing-harness') diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/permission/index/PermissionIndexer.java b/server/sonar-server-common/src/main/java/org/sonar/server/permission/index/PermissionIndexer.java new file mode 100644 index 00000000000..55100bfbb4d --- /dev/null +++ b/server/sonar-server-common/src/main/java/org/sonar/server/permission/index/PermissionIndexer.java @@ -0,0 +1,195 @@ +/* + * 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.permission.index; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableSet; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.es.EsQueueDto; +import org.sonar.server.es.BulkIndexer; +import org.sonar.server.es.BulkIndexer.Size; +import org.sonar.server.es.EsClient; +import org.sonar.server.es.EventIndexer; +import org.sonar.server.es.IndexType; +import org.sonar.server.es.Indexers; +import org.sonar.server.es.IndexingResult; +import org.sonar.server.es.OneToOneResilientIndexingListener; +import org.springframework.beans.factory.annotation.Autowired; + +import static java.util.Collections.emptyList; + +/** + * Populates the types "authorization" of each index requiring entity + * authorization. + */ +public class PermissionIndexer implements EventIndexer { + private final DbClient dbClient; + private final EsClient esClient; + private final Collection authorizationScopes; + private final Map indexTypeByFormat; + + @Autowired(required = false) + public PermissionIndexer(DbClient dbClient, EsClient esClient, NeedAuthorizationIndexer... needAuthorizationIndexers) { + this(dbClient, esClient, Arrays.stream(needAuthorizationIndexers) + .map(NeedAuthorizationIndexer::getAuthorizationScope) + .toList()); + } + + @VisibleForTesting + @Autowired(required = false) + public PermissionIndexer(DbClient dbClient, EsClient esClient, Collection authorizationScopes) { + this.dbClient = dbClient; + this.esClient = esClient; + this.authorizationScopes = authorizationScopes; + this.indexTypeByFormat = authorizationScopes.stream() + .map(AuthorizationScope::getIndexType) + .collect(Collectors.toMap(IndexType.IndexMainType::format, Function.identity())); + } + + @Override + public Set getIndexTypes() { + return ImmutableSet.copyOf(indexTypeByFormat.values()); + } + + @Override + public void indexOnStartup(Set uninitializedIndexTypes) { + // TODO do not load everything in memory. Db rows should be scrolled. + List authorizations = getAllAuthorizations(); + Stream scopes = getScopes(uninitializedIndexTypes); + index(authorizations, scopes, Size.LARGE); + } + + public void indexAll(Set uninitializedIndexTypes) { + // TODO do not load everything in memory. Db rows should be scrolled. + List authorizations = getAllAuthorizations(); + Stream scopes = getScopes(uninitializedIndexTypes); + index(authorizations, scopes, Size.REGULAR); + } + + @VisibleForTesting + void index(List authorizations) { + index(authorizations, authorizationScopes.stream(), Size.REGULAR); + } + + @Override + public Collection prepareForRecoveryOnEntityEvent(DbSession dbSession, Collection entityUuids, Indexers.EntityEvent cause) { + return switch (cause) { + case PROJECT_KEY_UPDATE, PROJECT_TAGS_UPDATE -> + // nothing to change. project key and tags are not part of this index + emptyList(); + case CREATION, DELETION, PERMISSION_CHANGE -> insertIntoEsQueue(dbSession, entityUuids); + }; + } + + @Override + public Collection prepareForRecoveryOnBranchEvent(DbSession dbSession, Collection branchUuids, Indexers.BranchEvent cause) { + return emptyList(); + } + + private Collection insertIntoEsQueue(DbSession dbSession, Collection projectUuids) { + List items = indexTypeByFormat.values().stream() + .flatMap(indexType -> projectUuids.stream().map(projectUuid -> EsQueueDto.create(indexType.format(), AuthorizationDoc.idOf(projectUuid), null, projectUuid))) + .toList(); + + dbClient.esQueueDao().insert(dbSession, items); + return items; + } + + private void index(Collection authorizations, Stream scopes, Size bulkSize) { + if (authorizations.isEmpty()) { + return; + } + + // index each authorization in each scope + scopes.forEach(scope -> { + IndexType indexType = scope.getIndexType(); + + BulkIndexer bulkIndexer = new BulkIndexer(esClient, indexType, bulkSize); + bulkIndexer.start(); + + authorizations.stream() + .filter(scope.getEntityPredicate()) + .map(dto -> AuthorizationDoc.fromDto(indexType, dto).toIndexRequest()) + .forEach(bulkIndexer::add); + + bulkIndexer.stop(); + }); + } + + @Override + public IndexingResult index(DbSession dbSession, Collection items) { + IndexingResult result = new IndexingResult(); + + List bulkIndexers = items.stream() + .map(EsQueueDto::getDocType) + .distinct() + .map(indexTypeByFormat::get) + .filter(Objects::nonNull) + .map(indexType -> new BulkIndexer(esClient, indexType, Size.REGULAR, new OneToOneResilientIndexingListener(dbClient, dbSession, items))) + .toList(); + + if (bulkIndexers.isEmpty()) { + return result; + } + + bulkIndexers.forEach(BulkIndexer::start); + + PermissionIndexerDao permissionIndexerDao = new PermissionIndexerDao(); + Set remainingEntityUuids = items.stream().map(EsQueueDto::getDocId) + .map(AuthorizationDoc::entityUuidOf) + .collect(Collectors.toSet()); + permissionIndexerDao.selectByUuids(dbClient, dbSession, remainingEntityUuids).forEach(p -> { + remainingEntityUuids.remove(p.getEntityUuid()); + bulkIndexers.forEach(bi -> bi.add(AuthorizationDoc.fromDto(bi.getIndexType(), p).toIndexRequest())); + }); + + // the remaining references on entities that don't exist in db. They must + // be deleted from the index. + remainingEntityUuids.forEach(entityUuid -> bulkIndexers.forEach(bi -> { + String authorizationDocId = AuthorizationDoc.idOf(entityUuid); + bi.addDeletion(bi.getIndexType(), authorizationDocId, authorizationDocId); + })); + + bulkIndexers.forEach(b -> result.add(b.stop())); + + return result; + } + + private Stream getScopes(Set indexTypes) { + return authorizationScopes.stream() + .filter(scope -> indexTypes.contains(scope.getIndexType())); + } + + private List getAllAuthorizations() { + try (DbSession dbSession = dbClient.openSession(false)) { + return new PermissionIndexerDao().selectAll(dbClient, dbSession); + } + } +} diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/permission/index/PermissionIndexerDao.java b/server/sonar-server-common/src/main/java/org/sonar/server/permission/index/PermissionIndexerDao.java new file mode 100644 index 00000000000..14656ebf5c8 --- /dev/null +++ b/server/sonar-server-common/src/main/java/org/sonar/server/permission/index/PermissionIndexerDao.java @@ -0,0 +1,180 @@ +/* + * 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.permission.index; + +import com.google.common.collect.ImmutableList; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.commons.lang.StringUtils; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; + +import static org.apache.commons.lang.StringUtils.repeat; +import static org.sonar.db.DatabaseUtils.executeLargeInputs; + +/** + * No streaming because of union of joins -> no need to use ResultSetIterator + */ +public class PermissionIndexerDao { + + private enum RowKind { + USER, GROUP, ANYONE, NONE + } + + private static final String SQL_TEMPLATE = """ + with entity as ((select prj.uuid as uuid, + prj.private as isPrivate, + prj.qualifier as qualifier + from projects prj) + union + (select p.uuid as uuid, + p.private as isPrivate, + 'VW' as qualifier + from portfolios p + where p.parent_uuid is null)) + SELECT entity_authorization.kind as kind, + entity_authorization.entity as entity, + entity_authorization.user_uuid as user_uuid, + entity_authorization.group_uuid as group_uuid, + entity_authorization.qualifier as qualifier + FROM (SELECT '%s' as kind, + e.uuid AS entity, + e.qualifier AS qualifier, + user_roles.user_uuid AS user_uuid, + NULL AS group_uuid + FROM entity e + INNER JOIN user_roles ON user_roles.entity_uuid = e.uuid AND user_roles.role = 'user' + WHERE (1 = 1) + {entitiesCondition} + UNION + SELECT '%s' as kind, e.uuid AS entity, e.qualifier AS qualifier, NULL AS user_uuid, groups.uuid AS group_uuid + FROM entity e + INNER JOIN group_roles + ON group_roles.entity_uuid = e.uuid AND group_roles.role = 'user' + INNER JOIN groups ON groups.uuid = group_roles.group_uuid + WHERE group_uuid IS NOT NULL + {entitiesCondition} + UNION + SELECT '%s' as kind, e.uuid AS entity, e.qualifier AS qualifier, NULL AS user_uuid, NULL AS group_uuid + FROM entity e + WHERE e.isPrivate = ? + {entitiesCondition} + UNION + SELECT '%s' as kind, e.uuid AS entity, e.qualifier AS qualifier, NULL AS user_uuid, NULL AS group_uuid + FROM entity e + WHERE e.isPrivate = ? + {entitiesCondition} + ) entity_authorization""".formatted(RowKind.USER, RowKind.GROUP, RowKind.ANYONE, RowKind.NONE); + + List selectAll(DbClient dbClient, DbSession session) { + return doSelectByEntities(dbClient, session, Collections.emptyList()); + } + + public List selectByUuids(DbClient dbClient, DbSession session, Collection entitiesUuid) { + // we use a smaller partitionSize because the SQL_TEMPLATE contain 4x the list of entity uuid. + // the MsSQL jdbc driver accept a maximum of 2100 prepareStatement parameter. To stay under the limit, + // we go with batch of 1000/2=500 entities uuids, to stay under the limit (4x500 < 2100) + return executeLargeInputs(entitiesUuid, entity -> doSelectByEntities(dbClient, session, entity), i -> i / 2); + } + + private static List doSelectByEntities(DbClient dbClient, DbSession session, List entitiesUuids) { + try { + Map dtosByEntityUuid = new HashMap<>(); + try (PreparedStatement stmt = createStatement(dbClient, session, entitiesUuids); + ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + processRow(rs, dtosByEntityUuid); + } + return ImmutableList.copyOf(dtosByEntityUuid.values()); + } + } catch (SQLException e) { + throw new IllegalStateException("Fail to select authorizations", e); + } + } + + private static PreparedStatement createStatement(DbClient dbClient, DbSession session, List entityUuids) throws SQLException { + String sql; + if (entityUuids.isEmpty()) { + sql = StringUtils.replace(SQL_TEMPLATE, "{entitiesCondition}", ""); + } else { + sql = StringUtils.replace(SQL_TEMPLATE, "{entitiesCondition}", " AND e.uuid in (" + repeat("?", ", ", entityUuids.size()) + ")"); + } + PreparedStatement stmt = dbClient.getMyBatis().newScrollingSelectStatement(session, sql); + int index = 1; + // query for RowKind.USER + index = populateEntityUuidPlaceholders(stmt, entityUuids, index); + // query for RowKind.GROUP + index = populateEntityUuidPlaceholders(stmt, entityUuids, index); + // query for RowKind.ANYONE + index = setPrivateEntityPlaceHolder(stmt, index, false); + index = populateEntityUuidPlaceholders(stmt, entityUuids, index); + // query for RowKind.NONE + index = setPrivateEntityPlaceHolder(stmt, index, true); + populateEntityUuidPlaceholders(stmt, entityUuids, index); + return stmt; + } + + private static int populateEntityUuidPlaceholders(PreparedStatement stmt, List entityUuids, int index) throws SQLException { + int newIndex = index; + for (String entityUuid : entityUuids) { + stmt.setString(newIndex, entityUuid); + newIndex++; + } + return newIndex; + } + + private static int setPrivateEntityPlaceHolder(PreparedStatement stmt, int index, boolean isPrivate) throws SQLException { + int newIndex = index; + stmt.setBoolean(newIndex, isPrivate); + newIndex++; + return newIndex; + } + + private static void processRow(ResultSet rs, Map dtosByEntityUuid) throws SQLException { + RowKind rowKind = RowKind.valueOf(rs.getString(1)); + String entityUuid = rs.getString(2); + + IndexPermissions dto = dtosByEntityUuid.get(entityUuid); + if (dto == null) { + String qualifier = rs.getString(5); + dto = new IndexPermissions(entityUuid, qualifier); + dtosByEntityUuid.put(entityUuid, dto); + } + switch (rowKind) { + case NONE: + break; + case USER: + dto.addUserUuid(rs.getString(3)); + break; + case GROUP: + dto.addGroupUuid(rs.getString(4)); + break; + case ANYONE: + dto.allowAnyone(); + break; + } + } +} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/permission/index/PermissionIndexerDaoIT.java b/server/sonar-server-common/src/test/java/org/sonar/server/permission/index/PermissionIndexerDaoIT.java new file mode 100644 index 00000000000..8bc2b38bc0f --- /dev/null +++ b/server/sonar-server-common/src/test/java/org/sonar/server/permission/index/PermissionIndexerDaoIT.java @@ -0,0 +1,272 @@ +/* + * 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.permission.index; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.assertj.core.api.Assertions; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.utils.System2; +import org.sonar.core.util.Uuids; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; +import org.sonar.db.component.ProjectData; +import org.sonar.db.permission.GroupPermissionDto; +import org.sonar.db.portfolio.PortfolioDto; +import org.sonar.db.project.ProjectDto; +import org.sonar.db.user.GroupDto; +import org.sonar.db.user.UserDbTester; +import org.sonar.db.user.UserDto; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.api.resources.Qualifiers.APP; +import static org.sonar.api.resources.Qualifiers.PROJECT; +import static org.sonar.api.resources.Qualifiers.VIEW; +import static org.sonar.api.web.UserRole.ADMIN; +import static org.sonar.api.web.UserRole.USER; + +public class PermissionIndexerDaoIT { + + @Rule + public DbTester dbTester = DbTester.create(System2.INSTANCE); + + private final DbClient dbClient = dbTester.getDbClient(); + private final DbSession dbSession = dbTester.getSession(); + private final UserDbTester userDbTester = new UserDbTester(dbTester); + + private ProjectDto publicProject; + private ProjectDto privateProject1; + private ProjectDto privateProject2; + private PortfolioDto view1; + private PortfolioDto view2; + private ProjectDto application; + private UserDto user1; + private UserDto user2; + private GroupDto group; + + private final PermissionIndexerDao underTest = new PermissionIndexerDao(); + + @Before + public void setUp() { + publicProject = dbTester.components().insertPublicProject().getProjectDto(); + privateProject1 = dbTester.components().insertPrivateProject().getProjectDto(); + privateProject2 = dbTester.components().insertPrivateProject().getProjectDto(); + view1 = dbTester.components().insertPublicPortfolioDto(); + view2 = dbTester.components().insertPublicPortfolioDto(); + application = dbTester.components().insertPublicApplication().getProjectDto(); + user1 = userDbTester.insertUser(); + user2 = userDbTester.insertUser(); + group = userDbTester.insertGroup(); + } + + @Test + public void select_all() { + insertTestDataForProjectsAndViews(); + + Collection dtos = underTest.selectAll(dbClient, dbSession); + Assertions.assertThat(dtos).hasSize(6); + + IndexPermissions publicProjectAuthorization = getByProjectUuid(publicProject.getUuid(), dtos); + isPublic(publicProjectAuthorization, PROJECT); + + IndexPermissions view1Authorization = getByProjectUuid(view1.getUuid(), dtos); + isPublic(view1Authorization, VIEW); + + IndexPermissions applicationAuthorization = getByProjectUuid(application.getUuid(), dtos); + isPublic(applicationAuthorization, APP); + + IndexPermissions privateProject1Authorization = getByProjectUuid(privateProject1.getUuid(), dtos); + assertThat(privateProject1Authorization.getGroupUuids()).containsOnly(group.getUuid()); + assertThat(privateProject1Authorization.isAllowAnyone()).isFalse(); + assertThat(privateProject1Authorization.getUserUuids()).containsOnly(user1.getUuid(), user2.getUuid()); + assertThat(privateProject1Authorization.getQualifier()).isEqualTo(PROJECT); + + IndexPermissions privateProject2Authorization = getByProjectUuid(privateProject2.getUuid(), dtos); + assertThat(privateProject2Authorization.getGroupUuids()).isEmpty(); + assertThat(privateProject2Authorization.isAllowAnyone()).isFalse(); + assertThat(privateProject2Authorization.getUserUuids()).containsOnly(user1.getUuid()); + assertThat(privateProject2Authorization.getQualifier()).isEqualTo(PROJECT); + + IndexPermissions view2Authorization = getByProjectUuid(view2.getUuid(), dtos); + isPublic(view2Authorization, VIEW); + } + + @Test + public void selectByUuids() { + insertTestDataForProjectsAndViews(); + + Map dtos = underTest + .selectByUuids(dbClient, dbSession, + asList(publicProject.getUuid(), privateProject1.getUuid(), privateProject2.getUuid(), view1.getUuid(), view2.getUuid(), application.getUuid())) + .stream() + .collect(Collectors.toMap(IndexPermissions::getEntityUuid, Function.identity())); + Assertions.assertThat(dtos).hasSize(6); + + IndexPermissions publicProjectAuthorization = dtos.get(publicProject.getUuid()); + isPublic(publicProjectAuthorization, PROJECT); + + IndexPermissions view1Authorization = dtos.get(view1.getUuid()); + isPublic(view1Authorization, VIEW); + + IndexPermissions applicationAuthorization = dtos.get(application.getUuid()); + isPublic(applicationAuthorization, APP); + + IndexPermissions privateProject1Authorization = dtos.get(privateProject1.getUuid()); + assertThat(privateProject1Authorization.getGroupUuids()).containsOnly(group.getUuid()); + assertThat(privateProject1Authorization.isAllowAnyone()).isFalse(); + assertThat(privateProject1Authorization.getUserUuids()).containsOnly(user1.getUuid(), user2.getUuid()); + assertThat(privateProject1Authorization.getQualifier()).isEqualTo(PROJECT); + + IndexPermissions privateProject2Authorization = dtos.get(privateProject2.getUuid()); + assertThat(privateProject2Authorization.getGroupUuids()).isEmpty(); + assertThat(privateProject2Authorization.isAllowAnyone()).isFalse(); + assertThat(privateProject2Authorization.getUserUuids()).containsOnly(user1.getUuid()); + assertThat(privateProject2Authorization.getQualifier()).isEqualTo(PROJECT); + + IndexPermissions view2Authorization = dtos.get(view2.getUuid()); + isPublic(view2Authorization, VIEW); + } + + @Test + public void selectByUuids_returns_empty_list_when_project_does_not_exist() { + insertTestDataForProjectsAndViews(); + + List dtos = underTest.selectByUuids(dbClient, dbSession, singletonList("missing")); + Assertions.assertThat(dtos).isEmpty(); + } + + @Test + public void select_by_projects_with_high_number_of_projects() { + List projectUuids = new ArrayList<>(); + for (int i = 0; i < 3500; i++) { + ProjectData project = dbTester.components().insertPrivateProject(Integer.toString(i)); + projectUuids.add(project.projectUuid()); + GroupPermissionDto dto = new GroupPermissionDto() + .setUuid(Uuids.createFast()) + .setGroupUuid(group.getUuid()) + .setGroupName(group.getName()) + .setRole(USER) + .setEntityUuid(project.projectUuid()) + .setEntityName(project.getProjectDto().getName()); + dbClient.groupPermissionDao().insert(dbSession, dto, project.getProjectDto(), null); + } + dbSession.commit(); + + assertThat(underTest.selectByUuids(dbClient, dbSession, projectUuids)) + .hasSize(3500) + .extracting(IndexPermissions::getEntityUuid) + .containsAll(projectUuids); + } + + @Test + public void return_private_project_without_any_permission_when_no_permission_in_DB() { + List dtos = underTest.selectByUuids(dbClient, dbSession, singletonList(privateProject1.getUuid())); + + // no permissions + Assertions.assertThat(dtos).hasSize(1); + IndexPermissions dto = dtos.get(0); + assertThat(dto.getGroupUuids()).isEmpty(); + assertThat(dto.getUserUuids()).isEmpty(); + assertThat(dto.isAllowAnyone()).isFalse(); + assertThat(dto.getEntityUuid()).isEqualTo(privateProject1.getUuid()); + assertThat(dto.getQualifier()).isEqualTo(privateProject1.getQualifier()); + } + + @Test + public void return_public_project_with_only_AllowAnyone_true_when_no_permission_in_DB() { + List dtos = underTest.selectByUuids(dbClient, dbSession, singletonList(publicProject.getUuid())); + + Assertions.assertThat(dtos).hasSize(1); + IndexPermissions dto = dtos.get(0); + assertThat(dto.getGroupUuids()).isEmpty(); + assertThat(dto.getUserUuids()).isEmpty(); + assertThat(dto.isAllowAnyone()).isTrue(); + assertThat(dto.getEntityUuid()).isEqualTo(publicProject.getUuid()); + assertThat(dto.getQualifier()).isEqualTo(publicProject.getQualifier()); + } + + @Test + public void return_private_project_with_AllowAnyone_false_and_user_id_when_user_is_granted_USER_permission_directly() { + dbTester.users().insertProjectPermissionOnUser(user1, USER, privateProject1); + List dtos = underTest.selectByUuids(dbClient, dbSession, singletonList(privateProject1.getUuid())); + + Assertions.assertThat(dtos).hasSize(1); + IndexPermissions dto = dtos.get(0); + assertThat(dto.getGroupUuids()).isEmpty(); + assertThat(dto.getUserUuids()).containsOnly(user1.getUuid()); + assertThat(dto.isAllowAnyone()).isFalse(); + assertThat(dto.getEntityUuid()).isEqualTo(privateProject1.getUuid()); + assertThat(dto.getQualifier()).isEqualTo(privateProject1.getQualifier()); + } + + @Test + public void return_private_project_with_AllowAnyone_false_and_group_id_but_not_user_id_when_user_is_granted_USER_permission_through_group() { + dbTester.users().insertMember(group, user1); + dbTester.users().insertEntityPermissionOnGroup(group, USER, privateProject1); + List dtos = underTest.selectByUuids(dbClient, dbSession, singletonList(privateProject1.getUuid())); + + Assertions.assertThat(dtos).hasSize(1); + IndexPermissions dto = dtos.get(0); + assertThat(dto.getGroupUuids()).containsOnly(group.getUuid()); + assertThat(dto.getUserUuids()).isEmpty(); + assertThat(dto.isAllowAnyone()).isFalse(); + assertThat(dto.getEntityUuid()).isEqualTo(privateProject1.getUuid()); + assertThat(dto.getQualifier()).isEqualTo(privateProject1.getQualifier()); + } + + private void isPublic(IndexPermissions view1Authorization, String qualifier) { + assertThat(view1Authorization.getGroupUuids()).isEmpty(); + assertThat(view1Authorization.isAllowAnyone()).isTrue(); + assertThat(view1Authorization.getUserUuids()).isEmpty(); + assertThat(view1Authorization.getQualifier()).isEqualTo(qualifier); + } + + private static IndexPermissions getByProjectUuid(String projectUuid, Collection dtos) { + return dtos.stream().filter(dto -> dto.getEntityUuid().equals(projectUuid)).findFirst().orElseThrow(IllegalArgumentException::new); + } + + private void insertTestDataForProjectsAndViews() { + // user1 has USER access on both private projects + userDbTester.insertProjectPermissionOnUser(user1, ADMIN, publicProject); + userDbTester.insertProjectPermissionOnUser(user1, USER, privateProject1); + userDbTester.insertProjectPermissionOnUser(user1, USER, privateProject2); + userDbTester.insertProjectPermissionOnUser(user1, ADMIN, view1); + userDbTester.insertProjectPermissionOnUser(user1, ADMIN, application); + + // user2 has USER access on privateProject1 only + userDbTester.insertProjectPermissionOnUser(user2, USER, privateProject1); + userDbTester.insertProjectPermissionOnUser(user2, ADMIN, privateProject2); + + // group1 has USER access on privateProject1 only + userDbTester.insertEntityPermissionOnGroup(group, USER, privateProject1); + userDbTester.insertEntityPermissionOnGroup(group, ADMIN, privateProject1); + userDbTester.insertEntityPermissionOnGroup(group, ADMIN, view1); + userDbTester.insertEntityPermissionOnGroup(group, ADMIN, application); + } +} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/permission/index/PermissionIndexerTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/permission/index/PermissionIndexerTest.java new file mode 100644 index 00000000000..7e93ed7f4d8 --- /dev/null +++ b/server/sonar-server-common/src/test/java/org/sonar/server/permission/index/PermissionIndexerTest.java @@ -0,0 +1,424 @@ +/* + * 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.permission.index; + +import java.util.Collection; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.utils.System2; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; +import org.sonar.db.component.ProjectData; +import org.sonar.db.entity.EntityDto; +import org.sonar.db.es.EsQueueDto; +import org.sonar.db.portfolio.PortfolioDto; +import org.sonar.db.project.ProjectDto; +import org.sonar.db.user.GroupDto; +import org.sonar.db.user.UserDto; +import org.sonar.server.es.EsTester; +import org.sonar.server.es.IndexType; +import org.sonar.server.es.IndexType.IndexMainType; +import org.sonar.server.es.Indexers.EntityEvent; +import org.sonar.server.es.IndexingResult; +import org.sonar.server.tester.UserSessionRule; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.api.resources.Qualifiers.PROJECT; +import static org.sonar.api.web.UserRole.ADMIN; +import static org.sonar.api.web.UserRole.USER; +import static org.sonar.server.es.Indexers.EntityEvent.PERMISSION_CHANGE; +import static org.sonar.server.permission.index.IndexAuthorizationConstants.TYPE_AUTHORIZATION; + +public class PermissionIndexerTest { + + private static final IndexMainType INDEX_TYPE_FOO_AUTH = IndexType.main(FooIndexDefinition.DESCRIPTOR, TYPE_AUTHORIZATION); + + @Rule + public DbTester db = DbTester.create(System2.INSTANCE); + @Rule + public EsTester es = EsTester.createCustom(new FooIndexDefinition()); + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + + private FooIndex fooIndex = new FooIndex(es.client(), new WebAuthorizationTypeSupport(userSession)); + private FooIndexer fooIndexer = new FooIndexer(es.client(), db.getDbClient()); + private PermissionIndexer underTest = new PermissionIndexer(db.getDbClient(), es.client(), fooIndexer); + + @Test + public void indexOnStartup_grants_access_to_any_user_and_to_group_Anyone_on_public_projects() { + ProjectDto project = createAndIndexPublicProject(); + UserDto user1 = db.users().insertUser(); + UserDto user2 = db.users().insertUser(); + + indexOnStartup(); + + verifyAnyoneAuthorized(project); + verifyAuthorized(project, user1); + verifyAuthorized(project, user2); + } + + @Test + public void indexAll_grants_access_to_any_user_and_to_group_Anyone_on_public_projects() { + ProjectDto project = createAndIndexPublicProject(); + UserDto user1 = db.users().insertUser(); + UserDto user2 = db.users().insertUser(); + + underTest.indexAll(underTest.getIndexTypes()); + + verifyAnyoneAuthorized(project); + verifyAuthorized(project, user1); + verifyAuthorized(project, user2); + } + + @Test + public void deletion_resilience_will_deindex_projects() { + ProjectDto project1 = createUnindexedPublicProject(); + ProjectDto project2 = createUnindexedPublicProject(); + // UserDto user1 = db.users().insertUser(); + indexOnStartup(); + assertThat(es.countDocuments(INDEX_TYPE_FOO_AUTH)).isEqualTo(2); + + // Simulate a indexation issue + db.getDbClient().purgeDao().deleteProject(db.getSession(), project1.getUuid(), PROJECT, project1.getName(), project1.getKey()); + underTest.prepareForRecoveryOnEntityEvent(db.getSession(), asList(project1.getUuid()), EntityEvent.DELETION); + assertThat(db.countRowsOfTable(db.getSession(), "es_queue")).isOne(); + Collection esQueueDtos = db.getDbClient().esQueueDao().selectForRecovery(db.getSession(), Long.MAX_VALUE, 2); + + underTest.index(db.getSession(), esQueueDtos); + + assertThat(db.countRowsOfTable(db.getSession(), "es_queue")).isZero(); + assertThat(es.countDocuments(INDEX_TYPE_FOO_AUTH)).isOne(); + } + + @Test + public void indexOnStartup_grants_access_to_user() { + ProjectDto project = createAndIndexPrivateProject(); + UserDto user1 = db.users().insertUser(); + UserDto user2 = db.users().insertUser(); + db.users().insertProjectPermissionOnUser(user1, USER, project); + db.users().insertProjectPermissionOnUser(user2, ADMIN, project); + + indexOnStartup(); + + // anonymous + verifyAnyoneNotAuthorized(project); + + // user1 has access + verifyAuthorized(project, user1); + + // user2 has not access (only USER permission is accepted) + verifyNotAuthorized(project, user2); + } + + @Test + public void indexOnStartup_grants_access_to_group_on_private_project() { + ProjectDto project = createAndIndexPrivateProject(); + UserDto user1 = db.users().insertUser(); + UserDto user2 = db.users().insertUser(); + UserDto user3 = db.users().insertUser(); + GroupDto group1 = db.users().insertGroup(); + GroupDto group2 = db.users().insertGroup(); + db.users().insertEntityPermissionOnGroup(group1, USER, project); + db.users().insertEntityPermissionOnGroup(group2, ADMIN, project); + + indexOnStartup(); + + // anonymous + verifyAnyoneNotAuthorized(project); + + // group1 has access + verifyAuthorized(project, user1, group1); + + // group2 has not access (only USER permission is accepted) + verifyNotAuthorized(project, user2, group2); + + // user3 is not in any group + verifyNotAuthorized(project, user3); + } + + @Test + public void indexOnStartup_grants_access_to_user_and_group() { + ProjectDto project = createAndIndexPrivateProject(); + UserDto user1 = db.users().insertUser(); + UserDto user2 = db.users().insertUser(); + GroupDto group = db.users().insertGroup(); + db.users().insertMember(group, user2); + db.users().insertProjectPermissionOnUser(user1, USER, project); + db.users().insertEntityPermissionOnGroup(group, USER, project); + + indexOnStartup(); + + // anonymous + verifyAnyoneNotAuthorized(project); + + // has direct access + verifyAuthorized(project, user1); + + // has access through group + verifyAuthorized(project, user1, group); + + // no access + verifyNotAuthorized(project, user2); + } + + @Test + public void indexOnStartup_does_not_grant_access_to_anybody_on_private_project() { + ProjectDto project = createAndIndexPrivateProject(); + UserDto user = db.users().insertUser(); + GroupDto group = db.users().insertGroup(); + + indexOnStartup(); + + verifyAnyoneNotAuthorized(project); + verifyNotAuthorized(project, user); + verifyNotAuthorized(project, user, group); + } + + @Test + public void indexOnStartup_grants_access_to_anybody_on_public_project() { + ProjectDto project = createAndIndexPublicProject(); + UserDto user = db.users().insertUser(); + GroupDto group = db.users().insertGroup(); + + indexOnStartup(); + + verifyAnyoneAuthorized(project); + verifyAuthorized(project, user); + verifyAuthorized(project, user, group); + } + + @Test + public void indexOnStartup_grants_access_to_anybody_on_view() { + PortfolioDto view = createAndIndexPortfolio(); + UserDto user = db.users().insertUser(); + GroupDto group = db.users().insertGroup(); + + indexOnStartup(); + + verifyAnyoneAuthorized(view); + verifyAuthorized(view, user); + verifyAuthorized(view, user, group); + } + + @Test + public void indexOnStartup_grants_access_on_many_projects() { + UserDto user1 = db.users().insertUser(); + UserDto user2 = db.users().insertUser(); + ProjectDto project = null; + for (int i = 0; i < 10; i++) { + project = createAndIndexPrivateProject(); + db.users().insertProjectPermissionOnUser(user1, USER, project); + } + + indexOnStartup(); + + verifyAnyoneNotAuthorized(project); + verifyAuthorized(project, user1); + verifyNotAuthorized(project, user2); + } + + @Test + public void public_projects_are_visible_to_anybody() { + ProjectDto projectOnOrg1 = createAndIndexPublicProject(); + UserDto user = db.users().insertUser(); + + indexOnStartup(); + + verifyAnyoneAuthorized(projectOnOrg1); + verifyAuthorized(projectOnOrg1, user); + } + + @Test + public void permissions_are_not_updated_on_project_tags_update() { + ProjectDto project = createAndIndexPublicProject(); + + indexPermissions(project, EntityEvent.PROJECT_TAGS_UPDATE); + + assertThatAuthIndexHasSize(0); + verifyAnyoneNotAuthorized(project); + } + + @Test + public void permissions_are_not_updated_on_project_key_update() { + ProjectDto project = createAndIndexPublicProject(); + + indexPermissions(project, EntityEvent.PROJECT_TAGS_UPDATE); + + assertThatAuthIndexHasSize(0); + verifyAnyoneNotAuthorized(project); + } + + @Test + public void index_permissions_on_project_creation() { + ProjectDto project = createAndIndexPrivateProject(); + UserDto user = db.users().insertUser(); + db.users().insertProjectPermissionOnUser(user, USER, project); + + indexPermissions(project, EntityEvent.CREATION); + + assertThatAuthIndexHasSize(1); + verifyAuthorized(project, user); + } + + @Test + public void index_permissions_on_permission_change() { + ProjectDto project = createAndIndexPrivateProject(); + UserDto user1 = db.users().insertUser(); + UserDto user2 = db.users().insertUser(); + db.users().insertProjectPermissionOnUser(user1, USER, project); + indexPermissions(project, EntityEvent.CREATION); + verifyAuthorized(project, user1); + verifyNotAuthorized(project, user2); + + db.users().insertProjectPermissionOnUser(user2, USER, project); + indexPermissions(project, PERMISSION_CHANGE); + + verifyAuthorized(project, user1); + verifyAuthorized(project, user1); + } + + @Test + public void delete_permissions_on_project_deletion() { + ProjectDto project = createAndIndexPrivateProject(); + UserDto user = db.users().insertUser(); + db.users().insertProjectPermissionOnUser(user, USER, project); + indexPermissions(project, EntityEvent.CREATION); + verifyAuthorized(project, user); + + db.getDbClient().purgeDao().deleteProject(db.getSession(), project.getUuid(), PROJECT, project.getUuid(), project.getKey()); + indexPermissions(project, EntityEvent.DELETION); + + verifyNotAuthorized(project, user); + assertThatAuthIndexHasSize(0); + } + + @Test + public void errors_during_indexing_are_recovered() { + ProjectDto project = createAndIndexPublicProject(); + es.lockWrites(INDEX_TYPE_FOO_AUTH); + + IndexingResult result = indexPermissions(project, PERMISSION_CHANGE); + assertThat(result.getTotal()).isOne(); + assertThat(result.getFailures()).isOne(); + + // index is still read-only, fail to recover + result = recover(); + assertThat(result.getTotal()).isOne(); + assertThat(result.getFailures()).isOne(); + assertThatAuthIndexHasSize(0); + assertThatEsQueueTableHasSize(1); + + es.unlockWrites(INDEX_TYPE_FOO_AUTH); + + result = recover(); + assertThat(result.getTotal()).isOne(); + assertThat(result.getFailures()).isZero(); + verifyAnyoneAuthorized(project); + assertThatEsQueueTableHasSize(0); + } + + private void assertThatAuthIndexHasSize(int expectedSize) { + assertThat(es.countDocuments(FooIndexDefinition.TYPE_AUTHORIZATION)).isEqualTo(expectedSize); + } + + private void indexOnStartup() { + underTest.indexOnStartup(underTest.getIndexTypes()); + } + + private void verifyAuthorized(EntityDto entity, UserDto user) { + logIn(user); + verifyAuthorized(entity, true); + } + + private void verifyAuthorized(EntityDto entity, UserDto user, GroupDto group) { + logIn(user).setGroups(group); + verifyAuthorized(entity, true); + } + + private void verifyNotAuthorized(EntityDto entity, UserDto user) { + logIn(user); + verifyAuthorized(entity, false); + } + + private void verifyNotAuthorized(EntityDto entity, UserDto user, GroupDto group) { + logIn(user).setGroups(group); + verifyAuthorized(entity, false); + } + + private void verifyAnyoneAuthorized(EntityDto entity) { + userSession.anonymous(); + verifyAuthorized(entity, true); + } + + private void verifyAnyoneNotAuthorized(EntityDto entity) { + userSession.anonymous(); + verifyAuthorized(entity, false); + } + + private void verifyAuthorized(EntityDto entity, boolean expectedAccess) { + assertThat(fooIndex.hasAccessToProject(entity.getUuid())).isEqualTo(expectedAccess); + } + + private UserSessionRule logIn(UserDto u) { + userSession.logIn(u); + return userSession; + } + + private IndexingResult indexPermissions(EntityDto entity, EntityEvent cause) { + DbSession dbSession = db.getSession(); + Collection items = underTest.prepareForRecoveryOnEntityEvent(dbSession, singletonList(entity.getUuid()), cause); + dbSession.commit(); + return underTest.index(dbSession, items); + } + + private ProjectDto createUnindexedPublicProject() { + return db.components().insertPublicProject().getProjectDto(); + } + + private ProjectDto createAndIndexPrivateProject() { + ProjectData project = db.components().insertPrivateProject(); + fooIndexer.indexOnAnalysis(project.getMainBranchDto().getUuid()); + return project.getProjectDto(); + } + + private ProjectDto createAndIndexPublicProject() { + ProjectData project = db.components().insertPublicProject(); + fooIndexer.indexOnAnalysis(project.getMainBranchDto().getUuid()); + return project.getProjectDto(); + } + + private PortfolioDto createAndIndexPortfolio() { + PortfolioDto view = db.components().insertPublicPortfolioDto(); + fooIndexer.indexOnAnalysis(view.getUuid()); + return view; + } + + private IndexingResult recover() { + Collection items = db.getDbClient().esQueueDao().selectForRecovery(db.getSession(), System.currentTimeMillis() + 1_000L, 10); + return underTest.index(db.getSession(), items); + } + + private void assertThatEsQueueTableHasSize(int expectedSize) { + assertThat(db.countRowsOfTable("es_queue")).isEqualTo(expectedSize); + } + +} diff --git a/server/sonar-webserver-es/src/it/java/org/sonar/server/permission/index/PermissionIndexerDaoIT.java b/server/sonar-webserver-es/src/it/java/org/sonar/server/permission/index/PermissionIndexerDaoIT.java deleted file mode 100644 index 8bc2b38bc0f..00000000000 --- a/server/sonar-webserver-es/src/it/java/org/sonar/server/permission/index/PermissionIndexerDaoIT.java +++ /dev/null @@ -1,272 +0,0 @@ -/* - * 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.permission.index; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; -import org.assertj.core.api.Assertions; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.sonar.api.utils.System2; -import org.sonar.core.util.Uuids; -import org.sonar.db.DbClient; -import org.sonar.db.DbSession; -import org.sonar.db.DbTester; -import org.sonar.db.component.ProjectData; -import org.sonar.db.permission.GroupPermissionDto; -import org.sonar.db.portfolio.PortfolioDto; -import org.sonar.db.project.ProjectDto; -import org.sonar.db.user.GroupDto; -import org.sonar.db.user.UserDbTester; -import org.sonar.db.user.UserDto; - -import static java.util.Arrays.asList; -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.sonar.api.resources.Qualifiers.APP; -import static org.sonar.api.resources.Qualifiers.PROJECT; -import static org.sonar.api.resources.Qualifiers.VIEW; -import static org.sonar.api.web.UserRole.ADMIN; -import static org.sonar.api.web.UserRole.USER; - -public class PermissionIndexerDaoIT { - - @Rule - public DbTester dbTester = DbTester.create(System2.INSTANCE); - - private final DbClient dbClient = dbTester.getDbClient(); - private final DbSession dbSession = dbTester.getSession(); - private final UserDbTester userDbTester = new UserDbTester(dbTester); - - private ProjectDto publicProject; - private ProjectDto privateProject1; - private ProjectDto privateProject2; - private PortfolioDto view1; - private PortfolioDto view2; - private ProjectDto application; - private UserDto user1; - private UserDto user2; - private GroupDto group; - - private final PermissionIndexerDao underTest = new PermissionIndexerDao(); - - @Before - public void setUp() { - publicProject = dbTester.components().insertPublicProject().getProjectDto(); - privateProject1 = dbTester.components().insertPrivateProject().getProjectDto(); - privateProject2 = dbTester.components().insertPrivateProject().getProjectDto(); - view1 = dbTester.components().insertPublicPortfolioDto(); - view2 = dbTester.components().insertPublicPortfolioDto(); - application = dbTester.components().insertPublicApplication().getProjectDto(); - user1 = userDbTester.insertUser(); - user2 = userDbTester.insertUser(); - group = userDbTester.insertGroup(); - } - - @Test - public void select_all() { - insertTestDataForProjectsAndViews(); - - Collection dtos = underTest.selectAll(dbClient, dbSession); - Assertions.assertThat(dtos).hasSize(6); - - IndexPermissions publicProjectAuthorization = getByProjectUuid(publicProject.getUuid(), dtos); - isPublic(publicProjectAuthorization, PROJECT); - - IndexPermissions view1Authorization = getByProjectUuid(view1.getUuid(), dtos); - isPublic(view1Authorization, VIEW); - - IndexPermissions applicationAuthorization = getByProjectUuid(application.getUuid(), dtos); - isPublic(applicationAuthorization, APP); - - IndexPermissions privateProject1Authorization = getByProjectUuid(privateProject1.getUuid(), dtos); - assertThat(privateProject1Authorization.getGroupUuids()).containsOnly(group.getUuid()); - assertThat(privateProject1Authorization.isAllowAnyone()).isFalse(); - assertThat(privateProject1Authorization.getUserUuids()).containsOnly(user1.getUuid(), user2.getUuid()); - assertThat(privateProject1Authorization.getQualifier()).isEqualTo(PROJECT); - - IndexPermissions privateProject2Authorization = getByProjectUuid(privateProject2.getUuid(), dtos); - assertThat(privateProject2Authorization.getGroupUuids()).isEmpty(); - assertThat(privateProject2Authorization.isAllowAnyone()).isFalse(); - assertThat(privateProject2Authorization.getUserUuids()).containsOnly(user1.getUuid()); - assertThat(privateProject2Authorization.getQualifier()).isEqualTo(PROJECT); - - IndexPermissions view2Authorization = getByProjectUuid(view2.getUuid(), dtos); - isPublic(view2Authorization, VIEW); - } - - @Test - public void selectByUuids() { - insertTestDataForProjectsAndViews(); - - Map dtos = underTest - .selectByUuids(dbClient, dbSession, - asList(publicProject.getUuid(), privateProject1.getUuid(), privateProject2.getUuid(), view1.getUuid(), view2.getUuid(), application.getUuid())) - .stream() - .collect(Collectors.toMap(IndexPermissions::getEntityUuid, Function.identity())); - Assertions.assertThat(dtos).hasSize(6); - - IndexPermissions publicProjectAuthorization = dtos.get(publicProject.getUuid()); - isPublic(publicProjectAuthorization, PROJECT); - - IndexPermissions view1Authorization = dtos.get(view1.getUuid()); - isPublic(view1Authorization, VIEW); - - IndexPermissions applicationAuthorization = dtos.get(application.getUuid()); - isPublic(applicationAuthorization, APP); - - IndexPermissions privateProject1Authorization = dtos.get(privateProject1.getUuid()); - assertThat(privateProject1Authorization.getGroupUuids()).containsOnly(group.getUuid()); - assertThat(privateProject1Authorization.isAllowAnyone()).isFalse(); - assertThat(privateProject1Authorization.getUserUuids()).containsOnly(user1.getUuid(), user2.getUuid()); - assertThat(privateProject1Authorization.getQualifier()).isEqualTo(PROJECT); - - IndexPermissions privateProject2Authorization = dtos.get(privateProject2.getUuid()); - assertThat(privateProject2Authorization.getGroupUuids()).isEmpty(); - assertThat(privateProject2Authorization.isAllowAnyone()).isFalse(); - assertThat(privateProject2Authorization.getUserUuids()).containsOnly(user1.getUuid()); - assertThat(privateProject2Authorization.getQualifier()).isEqualTo(PROJECT); - - IndexPermissions view2Authorization = dtos.get(view2.getUuid()); - isPublic(view2Authorization, VIEW); - } - - @Test - public void selectByUuids_returns_empty_list_when_project_does_not_exist() { - insertTestDataForProjectsAndViews(); - - List dtos = underTest.selectByUuids(dbClient, dbSession, singletonList("missing")); - Assertions.assertThat(dtos).isEmpty(); - } - - @Test - public void select_by_projects_with_high_number_of_projects() { - List projectUuids = new ArrayList<>(); - for (int i = 0; i < 3500; i++) { - ProjectData project = dbTester.components().insertPrivateProject(Integer.toString(i)); - projectUuids.add(project.projectUuid()); - GroupPermissionDto dto = new GroupPermissionDto() - .setUuid(Uuids.createFast()) - .setGroupUuid(group.getUuid()) - .setGroupName(group.getName()) - .setRole(USER) - .setEntityUuid(project.projectUuid()) - .setEntityName(project.getProjectDto().getName()); - dbClient.groupPermissionDao().insert(dbSession, dto, project.getProjectDto(), null); - } - dbSession.commit(); - - assertThat(underTest.selectByUuids(dbClient, dbSession, projectUuids)) - .hasSize(3500) - .extracting(IndexPermissions::getEntityUuid) - .containsAll(projectUuids); - } - - @Test - public void return_private_project_without_any_permission_when_no_permission_in_DB() { - List dtos = underTest.selectByUuids(dbClient, dbSession, singletonList(privateProject1.getUuid())); - - // no permissions - Assertions.assertThat(dtos).hasSize(1); - IndexPermissions dto = dtos.get(0); - assertThat(dto.getGroupUuids()).isEmpty(); - assertThat(dto.getUserUuids()).isEmpty(); - assertThat(dto.isAllowAnyone()).isFalse(); - assertThat(dto.getEntityUuid()).isEqualTo(privateProject1.getUuid()); - assertThat(dto.getQualifier()).isEqualTo(privateProject1.getQualifier()); - } - - @Test - public void return_public_project_with_only_AllowAnyone_true_when_no_permission_in_DB() { - List dtos = underTest.selectByUuids(dbClient, dbSession, singletonList(publicProject.getUuid())); - - Assertions.assertThat(dtos).hasSize(1); - IndexPermissions dto = dtos.get(0); - assertThat(dto.getGroupUuids()).isEmpty(); - assertThat(dto.getUserUuids()).isEmpty(); - assertThat(dto.isAllowAnyone()).isTrue(); - assertThat(dto.getEntityUuid()).isEqualTo(publicProject.getUuid()); - assertThat(dto.getQualifier()).isEqualTo(publicProject.getQualifier()); - } - - @Test - public void return_private_project_with_AllowAnyone_false_and_user_id_when_user_is_granted_USER_permission_directly() { - dbTester.users().insertProjectPermissionOnUser(user1, USER, privateProject1); - List dtos = underTest.selectByUuids(dbClient, dbSession, singletonList(privateProject1.getUuid())); - - Assertions.assertThat(dtos).hasSize(1); - IndexPermissions dto = dtos.get(0); - assertThat(dto.getGroupUuids()).isEmpty(); - assertThat(dto.getUserUuids()).containsOnly(user1.getUuid()); - assertThat(dto.isAllowAnyone()).isFalse(); - assertThat(dto.getEntityUuid()).isEqualTo(privateProject1.getUuid()); - assertThat(dto.getQualifier()).isEqualTo(privateProject1.getQualifier()); - } - - @Test - public void return_private_project_with_AllowAnyone_false_and_group_id_but_not_user_id_when_user_is_granted_USER_permission_through_group() { - dbTester.users().insertMember(group, user1); - dbTester.users().insertEntityPermissionOnGroup(group, USER, privateProject1); - List dtos = underTest.selectByUuids(dbClient, dbSession, singletonList(privateProject1.getUuid())); - - Assertions.assertThat(dtos).hasSize(1); - IndexPermissions dto = dtos.get(0); - assertThat(dto.getGroupUuids()).containsOnly(group.getUuid()); - assertThat(dto.getUserUuids()).isEmpty(); - assertThat(dto.isAllowAnyone()).isFalse(); - assertThat(dto.getEntityUuid()).isEqualTo(privateProject1.getUuid()); - assertThat(dto.getQualifier()).isEqualTo(privateProject1.getQualifier()); - } - - private void isPublic(IndexPermissions view1Authorization, String qualifier) { - assertThat(view1Authorization.getGroupUuids()).isEmpty(); - assertThat(view1Authorization.isAllowAnyone()).isTrue(); - assertThat(view1Authorization.getUserUuids()).isEmpty(); - assertThat(view1Authorization.getQualifier()).isEqualTo(qualifier); - } - - private static IndexPermissions getByProjectUuid(String projectUuid, Collection dtos) { - return dtos.stream().filter(dto -> dto.getEntityUuid().equals(projectUuid)).findFirst().orElseThrow(IllegalArgumentException::new); - } - - private void insertTestDataForProjectsAndViews() { - // user1 has USER access on both private projects - userDbTester.insertProjectPermissionOnUser(user1, ADMIN, publicProject); - userDbTester.insertProjectPermissionOnUser(user1, USER, privateProject1); - userDbTester.insertProjectPermissionOnUser(user1, USER, privateProject2); - userDbTester.insertProjectPermissionOnUser(user1, ADMIN, view1); - userDbTester.insertProjectPermissionOnUser(user1, ADMIN, application); - - // user2 has USER access on privateProject1 only - userDbTester.insertProjectPermissionOnUser(user2, USER, privateProject1); - userDbTester.insertProjectPermissionOnUser(user2, ADMIN, privateProject2); - - // group1 has USER access on privateProject1 only - userDbTester.insertEntityPermissionOnGroup(group, USER, privateProject1); - userDbTester.insertEntityPermissionOnGroup(group, ADMIN, privateProject1); - userDbTester.insertEntityPermissionOnGroup(group, ADMIN, view1); - userDbTester.insertEntityPermissionOnGroup(group, ADMIN, application); - } -} diff --git a/server/sonar-webserver-es/src/main/java/org/sonar/server/permission/index/PermissionIndexer.java b/server/sonar-webserver-es/src/main/java/org/sonar/server/permission/index/PermissionIndexer.java deleted file mode 100644 index 55100bfbb4d..00000000000 --- a/server/sonar-webserver-es/src/main/java/org/sonar/server/permission/index/PermissionIndexer.java +++ /dev/null @@ -1,195 +0,0 @@ -/* - * 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.permission.index; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableSet; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.sonar.db.DbClient; -import org.sonar.db.DbSession; -import org.sonar.db.es.EsQueueDto; -import org.sonar.server.es.BulkIndexer; -import org.sonar.server.es.BulkIndexer.Size; -import org.sonar.server.es.EsClient; -import org.sonar.server.es.EventIndexer; -import org.sonar.server.es.IndexType; -import org.sonar.server.es.Indexers; -import org.sonar.server.es.IndexingResult; -import org.sonar.server.es.OneToOneResilientIndexingListener; -import org.springframework.beans.factory.annotation.Autowired; - -import static java.util.Collections.emptyList; - -/** - * Populates the types "authorization" of each index requiring entity - * authorization. - */ -public class PermissionIndexer implements EventIndexer { - private final DbClient dbClient; - private final EsClient esClient; - private final Collection authorizationScopes; - private final Map indexTypeByFormat; - - @Autowired(required = false) - public PermissionIndexer(DbClient dbClient, EsClient esClient, NeedAuthorizationIndexer... needAuthorizationIndexers) { - this(dbClient, esClient, Arrays.stream(needAuthorizationIndexers) - .map(NeedAuthorizationIndexer::getAuthorizationScope) - .toList()); - } - - @VisibleForTesting - @Autowired(required = false) - public PermissionIndexer(DbClient dbClient, EsClient esClient, Collection authorizationScopes) { - this.dbClient = dbClient; - this.esClient = esClient; - this.authorizationScopes = authorizationScopes; - this.indexTypeByFormat = authorizationScopes.stream() - .map(AuthorizationScope::getIndexType) - .collect(Collectors.toMap(IndexType.IndexMainType::format, Function.identity())); - } - - @Override - public Set getIndexTypes() { - return ImmutableSet.copyOf(indexTypeByFormat.values()); - } - - @Override - public void indexOnStartup(Set uninitializedIndexTypes) { - // TODO do not load everything in memory. Db rows should be scrolled. - List authorizations = getAllAuthorizations(); - Stream scopes = getScopes(uninitializedIndexTypes); - index(authorizations, scopes, Size.LARGE); - } - - public void indexAll(Set uninitializedIndexTypes) { - // TODO do not load everything in memory. Db rows should be scrolled. - List authorizations = getAllAuthorizations(); - Stream scopes = getScopes(uninitializedIndexTypes); - index(authorizations, scopes, Size.REGULAR); - } - - @VisibleForTesting - void index(List authorizations) { - index(authorizations, authorizationScopes.stream(), Size.REGULAR); - } - - @Override - public Collection prepareForRecoveryOnEntityEvent(DbSession dbSession, Collection entityUuids, Indexers.EntityEvent cause) { - return switch (cause) { - case PROJECT_KEY_UPDATE, PROJECT_TAGS_UPDATE -> - // nothing to change. project key and tags are not part of this index - emptyList(); - case CREATION, DELETION, PERMISSION_CHANGE -> insertIntoEsQueue(dbSession, entityUuids); - }; - } - - @Override - public Collection prepareForRecoveryOnBranchEvent(DbSession dbSession, Collection branchUuids, Indexers.BranchEvent cause) { - return emptyList(); - } - - private Collection insertIntoEsQueue(DbSession dbSession, Collection projectUuids) { - List items = indexTypeByFormat.values().stream() - .flatMap(indexType -> projectUuids.stream().map(projectUuid -> EsQueueDto.create(indexType.format(), AuthorizationDoc.idOf(projectUuid), null, projectUuid))) - .toList(); - - dbClient.esQueueDao().insert(dbSession, items); - return items; - } - - private void index(Collection authorizations, Stream scopes, Size bulkSize) { - if (authorizations.isEmpty()) { - return; - } - - // index each authorization in each scope - scopes.forEach(scope -> { - IndexType indexType = scope.getIndexType(); - - BulkIndexer bulkIndexer = new BulkIndexer(esClient, indexType, bulkSize); - bulkIndexer.start(); - - authorizations.stream() - .filter(scope.getEntityPredicate()) - .map(dto -> AuthorizationDoc.fromDto(indexType, dto).toIndexRequest()) - .forEach(bulkIndexer::add); - - bulkIndexer.stop(); - }); - } - - @Override - public IndexingResult index(DbSession dbSession, Collection items) { - IndexingResult result = new IndexingResult(); - - List bulkIndexers = items.stream() - .map(EsQueueDto::getDocType) - .distinct() - .map(indexTypeByFormat::get) - .filter(Objects::nonNull) - .map(indexType -> new BulkIndexer(esClient, indexType, Size.REGULAR, new OneToOneResilientIndexingListener(dbClient, dbSession, items))) - .toList(); - - if (bulkIndexers.isEmpty()) { - return result; - } - - bulkIndexers.forEach(BulkIndexer::start); - - PermissionIndexerDao permissionIndexerDao = new PermissionIndexerDao(); - Set remainingEntityUuids = items.stream().map(EsQueueDto::getDocId) - .map(AuthorizationDoc::entityUuidOf) - .collect(Collectors.toSet()); - permissionIndexerDao.selectByUuids(dbClient, dbSession, remainingEntityUuids).forEach(p -> { - remainingEntityUuids.remove(p.getEntityUuid()); - bulkIndexers.forEach(bi -> bi.add(AuthorizationDoc.fromDto(bi.getIndexType(), p).toIndexRequest())); - }); - - // the remaining references on entities that don't exist in db. They must - // be deleted from the index. - remainingEntityUuids.forEach(entityUuid -> bulkIndexers.forEach(bi -> { - String authorizationDocId = AuthorizationDoc.idOf(entityUuid); - bi.addDeletion(bi.getIndexType(), authorizationDocId, authorizationDocId); - })); - - bulkIndexers.forEach(b -> result.add(b.stop())); - - return result; - } - - private Stream getScopes(Set indexTypes) { - return authorizationScopes.stream() - .filter(scope -> indexTypes.contains(scope.getIndexType())); - } - - private List getAllAuthorizations() { - try (DbSession dbSession = dbClient.openSession(false)) { - return new PermissionIndexerDao().selectAll(dbClient, dbSession); - } - } -} diff --git a/server/sonar-webserver-es/src/main/java/org/sonar/server/permission/index/PermissionIndexerDao.java b/server/sonar-webserver-es/src/main/java/org/sonar/server/permission/index/PermissionIndexerDao.java deleted file mode 100644 index 14656ebf5c8..00000000000 --- a/server/sonar-webserver-es/src/main/java/org/sonar/server/permission/index/PermissionIndexerDao.java +++ /dev/null @@ -1,180 +0,0 @@ -/* - * 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.permission.index; - -import com.google.common.collect.ImmutableList; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.apache.commons.lang.StringUtils; -import org.sonar.db.DbClient; -import org.sonar.db.DbSession; - -import static org.apache.commons.lang.StringUtils.repeat; -import static org.sonar.db.DatabaseUtils.executeLargeInputs; - -/** - * No streaming because of union of joins -> no need to use ResultSetIterator - */ -public class PermissionIndexerDao { - - private enum RowKind { - USER, GROUP, ANYONE, NONE - } - - private static final String SQL_TEMPLATE = """ - with entity as ((select prj.uuid as uuid, - prj.private as isPrivate, - prj.qualifier as qualifier - from projects prj) - union - (select p.uuid as uuid, - p.private as isPrivate, - 'VW' as qualifier - from portfolios p - where p.parent_uuid is null)) - SELECT entity_authorization.kind as kind, - entity_authorization.entity as entity, - entity_authorization.user_uuid as user_uuid, - entity_authorization.group_uuid as group_uuid, - entity_authorization.qualifier as qualifier - FROM (SELECT '%s' as kind, - e.uuid AS entity, - e.qualifier AS qualifier, - user_roles.user_uuid AS user_uuid, - NULL AS group_uuid - FROM entity e - INNER JOIN user_roles ON user_roles.entity_uuid = e.uuid AND user_roles.role = 'user' - WHERE (1 = 1) - {entitiesCondition} - UNION - SELECT '%s' as kind, e.uuid AS entity, e.qualifier AS qualifier, NULL AS user_uuid, groups.uuid AS group_uuid - FROM entity e - INNER JOIN group_roles - ON group_roles.entity_uuid = e.uuid AND group_roles.role = 'user' - INNER JOIN groups ON groups.uuid = group_roles.group_uuid - WHERE group_uuid IS NOT NULL - {entitiesCondition} - UNION - SELECT '%s' as kind, e.uuid AS entity, e.qualifier AS qualifier, NULL AS user_uuid, NULL AS group_uuid - FROM entity e - WHERE e.isPrivate = ? - {entitiesCondition} - UNION - SELECT '%s' as kind, e.uuid AS entity, e.qualifier AS qualifier, NULL AS user_uuid, NULL AS group_uuid - FROM entity e - WHERE e.isPrivate = ? - {entitiesCondition} - ) entity_authorization""".formatted(RowKind.USER, RowKind.GROUP, RowKind.ANYONE, RowKind.NONE); - - List selectAll(DbClient dbClient, DbSession session) { - return doSelectByEntities(dbClient, session, Collections.emptyList()); - } - - public List selectByUuids(DbClient dbClient, DbSession session, Collection entitiesUuid) { - // we use a smaller partitionSize because the SQL_TEMPLATE contain 4x the list of entity uuid. - // the MsSQL jdbc driver accept a maximum of 2100 prepareStatement parameter. To stay under the limit, - // we go with batch of 1000/2=500 entities uuids, to stay under the limit (4x500 < 2100) - return executeLargeInputs(entitiesUuid, entity -> doSelectByEntities(dbClient, session, entity), i -> i / 2); - } - - private static List doSelectByEntities(DbClient dbClient, DbSession session, List entitiesUuids) { - try { - Map dtosByEntityUuid = new HashMap<>(); - try (PreparedStatement stmt = createStatement(dbClient, session, entitiesUuids); - ResultSet rs = stmt.executeQuery()) { - while (rs.next()) { - processRow(rs, dtosByEntityUuid); - } - return ImmutableList.copyOf(dtosByEntityUuid.values()); - } - } catch (SQLException e) { - throw new IllegalStateException("Fail to select authorizations", e); - } - } - - private static PreparedStatement createStatement(DbClient dbClient, DbSession session, List entityUuids) throws SQLException { - String sql; - if (entityUuids.isEmpty()) { - sql = StringUtils.replace(SQL_TEMPLATE, "{entitiesCondition}", ""); - } else { - sql = StringUtils.replace(SQL_TEMPLATE, "{entitiesCondition}", " AND e.uuid in (" + repeat("?", ", ", entityUuids.size()) + ")"); - } - PreparedStatement stmt = dbClient.getMyBatis().newScrollingSelectStatement(session, sql); - int index = 1; - // query for RowKind.USER - index = populateEntityUuidPlaceholders(stmt, entityUuids, index); - // query for RowKind.GROUP - index = populateEntityUuidPlaceholders(stmt, entityUuids, index); - // query for RowKind.ANYONE - index = setPrivateEntityPlaceHolder(stmt, index, false); - index = populateEntityUuidPlaceholders(stmt, entityUuids, index); - // query for RowKind.NONE - index = setPrivateEntityPlaceHolder(stmt, index, true); - populateEntityUuidPlaceholders(stmt, entityUuids, index); - return stmt; - } - - private static int populateEntityUuidPlaceholders(PreparedStatement stmt, List entityUuids, int index) throws SQLException { - int newIndex = index; - for (String entityUuid : entityUuids) { - stmt.setString(newIndex, entityUuid); - newIndex++; - } - return newIndex; - } - - private static int setPrivateEntityPlaceHolder(PreparedStatement stmt, int index, boolean isPrivate) throws SQLException { - int newIndex = index; - stmt.setBoolean(newIndex, isPrivate); - newIndex++; - return newIndex; - } - - private static void processRow(ResultSet rs, Map dtosByEntityUuid) throws SQLException { - RowKind rowKind = RowKind.valueOf(rs.getString(1)); - String entityUuid = rs.getString(2); - - IndexPermissions dto = dtosByEntityUuid.get(entityUuid); - if (dto == null) { - String qualifier = rs.getString(5); - dto = new IndexPermissions(entityUuid, qualifier); - dtosByEntityUuid.put(entityUuid, dto); - } - switch (rowKind) { - case NONE: - break; - case USER: - dto.addUserUuid(rs.getString(3)); - break; - case GROUP: - dto.addGroupUuid(rs.getString(4)); - break; - case ANYONE: - dto.allowAnyone(); - break; - } - } -} diff --git a/server/sonar-webserver-es/src/test/java/org/sonar/server/permission/index/FooIndex.java b/server/sonar-webserver-es/src/test/java/org/sonar/server/permission/index/FooIndex.java deleted file mode 100644 index ab0ff8755eb..00000000000 --- a/server/sonar-webserver-es/src/test/java/org/sonar/server/permission/index/FooIndex.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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.permission.index; - -import java.util.Arrays; -import java.util.List; -import org.elasticsearch.index.query.QueryBuilders; -import org.elasticsearch.search.SearchHits; -import org.elasticsearch.search.builder.SearchSourceBuilder; -import org.sonar.server.es.EsClient; - -import static org.sonar.server.permission.index.FooIndexDefinition.DESCRIPTOR; - -public class FooIndex { - - private final EsClient esClient; - private final WebAuthorizationTypeSupport authorizationTypeSupport; - - public FooIndex(EsClient esClient, WebAuthorizationTypeSupport authorizationTypeSupport) { - this.esClient = esClient; - this.authorizationTypeSupport = authorizationTypeSupport; - } - - public boolean hasAccessToProject(String projectUuid) { - SearchHits hits = esClient.search(EsClient.prepareSearch(DESCRIPTOR.getName()) - .source(new SearchSourceBuilder().query(QueryBuilders.boolQuery() - .must(QueryBuilders.termQuery(FooIndexDefinition.FIELD_PROJECT_UUID, projectUuid)) - .filter(authorizationTypeSupport.createQueryFilter())))) - .getHits(); - List names = Arrays.stream(hits.getHits()) - .map(h -> h.getSourceAsMap().get(FooIndexDefinition.FIELD_NAME).toString()) - .toList(); - return names.size() == 2 && names.contains("bar") && names.contains("baz"); - } -} diff --git a/server/sonar-webserver-es/src/test/java/org/sonar/server/permission/index/FooIndexer.java b/server/sonar-webserver-es/src/test/java/org/sonar/server/permission/index/FooIndexer.java deleted file mode 100644 index cd8e1c4c262..00000000000 --- a/server/sonar-webserver-es/src/test/java/org/sonar/server/permission/index/FooIndexer.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * 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.permission.index; - -import java.util.Optional; -import java.util.Set; -import org.elasticsearch.action.index.IndexRequest; -import org.sonar.db.DbClient; -import org.sonar.db.DbSession; -import org.sonar.db.component.BranchDto; -import org.sonar.server.es.AnalysisIndexer; -import org.sonar.server.es.BaseDoc; -import org.sonar.server.es.EsClient; - -import static org.sonar.server.permission.index.FooIndexDefinition.TYPE_FOO; - -public class FooIndexer implements AnalysisIndexer, NeedAuthorizationIndexer { - - private static final AuthorizationScope AUTHORIZATION_SCOPE = new AuthorizationScope(TYPE_FOO, p -> true); - - private final EsClient esClient; - private final DbClient dbClient; - - public FooIndexer(EsClient esClient, DbClient dbClient) { - this.esClient = esClient; - this.dbClient = dbClient; - } - - @Override - public AuthorizationScope getAuthorizationScope() { - return AUTHORIZATION_SCOPE; - } - - @Override - public void indexOnAnalysis(String branchUuid) { - indexOnAnalysis(branchUuid, Set.of()); - } - - @Override - public void indexOnAnalysis(String branchUuid, Set unchangedComponentUuids) { - try(DbSession dbSession = dbClient.openSession(true)){ - Optional branchDto = dbClient.branchDao().selectByUuid(dbSession, branchUuid); - if (branchDto.isEmpty()) { - //For portfolio, adding branchUuid directly - addToIndex(branchUuid, "bar"); - addToIndex(branchUuid, "baz"); - }else{ - addToIndex(branchDto.get().getProjectUuid(), "bar"); - addToIndex(branchDto.get().getProjectUuid(), "baz"); - } - } - - - } - - private void addToIndex(String projectUuid, String name) { - FooDoc fooDoc = new FooDoc(projectUuid, name); - esClient.index(new IndexRequest(TYPE_FOO.getMainType().getIndex().getName()) - .type(TYPE_FOO.getMainType().getType()) - .id(fooDoc.getId()) - .routing(fooDoc.getRouting().orElse(null)) - .source(fooDoc.getFields())); - } - - private static final class FooDoc extends BaseDoc { - private final String projectUuid; - private final String name; - - private FooDoc(String projectUuid, String name) { - super(TYPE_FOO); - this.projectUuid = projectUuid; - this.name = name; - setField(FooIndexDefinition.FIELD_PROJECT_UUID, projectUuid); - setField(FooIndexDefinition.FIELD_NAME, name); - setParent(AuthorizationDoc.idOf(projectUuid)); - } - - @Override - public String getId() { - return projectUuid + "_" + name; - } - - } -} diff --git a/server/sonar-webserver-es/src/test/java/org/sonar/server/permission/index/PermissionIndexerTest.java b/server/sonar-webserver-es/src/test/java/org/sonar/server/permission/index/PermissionIndexerTest.java deleted file mode 100644 index 7e93ed7f4d8..00000000000 --- a/server/sonar-webserver-es/src/test/java/org/sonar/server/permission/index/PermissionIndexerTest.java +++ /dev/null @@ -1,424 +0,0 @@ -/* - * 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.permission.index; - -import java.util.Collection; -import org.junit.Rule; -import org.junit.Test; -import org.sonar.api.utils.System2; -import org.sonar.db.DbSession; -import org.sonar.db.DbTester; -import org.sonar.db.component.ProjectData; -import org.sonar.db.entity.EntityDto; -import org.sonar.db.es.EsQueueDto; -import org.sonar.db.portfolio.PortfolioDto; -import org.sonar.db.project.ProjectDto; -import org.sonar.db.user.GroupDto; -import org.sonar.db.user.UserDto; -import org.sonar.server.es.EsTester; -import org.sonar.server.es.IndexType; -import org.sonar.server.es.IndexType.IndexMainType; -import org.sonar.server.es.Indexers.EntityEvent; -import org.sonar.server.es.IndexingResult; -import org.sonar.server.tester.UserSessionRule; - -import static java.util.Arrays.asList; -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.sonar.api.resources.Qualifiers.PROJECT; -import static org.sonar.api.web.UserRole.ADMIN; -import static org.sonar.api.web.UserRole.USER; -import static org.sonar.server.es.Indexers.EntityEvent.PERMISSION_CHANGE; -import static org.sonar.server.permission.index.IndexAuthorizationConstants.TYPE_AUTHORIZATION; - -public class PermissionIndexerTest { - - private static final IndexMainType INDEX_TYPE_FOO_AUTH = IndexType.main(FooIndexDefinition.DESCRIPTOR, TYPE_AUTHORIZATION); - - @Rule - public DbTester db = DbTester.create(System2.INSTANCE); - @Rule - public EsTester es = EsTester.createCustom(new FooIndexDefinition()); - @Rule - public UserSessionRule userSession = UserSessionRule.standalone(); - - private FooIndex fooIndex = new FooIndex(es.client(), new WebAuthorizationTypeSupport(userSession)); - private FooIndexer fooIndexer = new FooIndexer(es.client(), db.getDbClient()); - private PermissionIndexer underTest = new PermissionIndexer(db.getDbClient(), es.client(), fooIndexer); - - @Test - public void indexOnStartup_grants_access_to_any_user_and_to_group_Anyone_on_public_projects() { - ProjectDto project = createAndIndexPublicProject(); - UserDto user1 = db.users().insertUser(); - UserDto user2 = db.users().insertUser(); - - indexOnStartup(); - - verifyAnyoneAuthorized(project); - verifyAuthorized(project, user1); - verifyAuthorized(project, user2); - } - - @Test - public void indexAll_grants_access_to_any_user_and_to_group_Anyone_on_public_projects() { - ProjectDto project = createAndIndexPublicProject(); - UserDto user1 = db.users().insertUser(); - UserDto user2 = db.users().insertUser(); - - underTest.indexAll(underTest.getIndexTypes()); - - verifyAnyoneAuthorized(project); - verifyAuthorized(project, user1); - verifyAuthorized(project, user2); - } - - @Test - public void deletion_resilience_will_deindex_projects() { - ProjectDto project1 = createUnindexedPublicProject(); - ProjectDto project2 = createUnindexedPublicProject(); - // UserDto user1 = db.users().insertUser(); - indexOnStartup(); - assertThat(es.countDocuments(INDEX_TYPE_FOO_AUTH)).isEqualTo(2); - - // Simulate a indexation issue - db.getDbClient().purgeDao().deleteProject(db.getSession(), project1.getUuid(), PROJECT, project1.getName(), project1.getKey()); - underTest.prepareForRecoveryOnEntityEvent(db.getSession(), asList(project1.getUuid()), EntityEvent.DELETION); - assertThat(db.countRowsOfTable(db.getSession(), "es_queue")).isOne(); - Collection esQueueDtos = db.getDbClient().esQueueDao().selectForRecovery(db.getSession(), Long.MAX_VALUE, 2); - - underTest.index(db.getSession(), esQueueDtos); - - assertThat(db.countRowsOfTable(db.getSession(), "es_queue")).isZero(); - assertThat(es.countDocuments(INDEX_TYPE_FOO_AUTH)).isOne(); - } - - @Test - public void indexOnStartup_grants_access_to_user() { - ProjectDto project = createAndIndexPrivateProject(); - UserDto user1 = db.users().insertUser(); - UserDto user2 = db.users().insertUser(); - db.users().insertProjectPermissionOnUser(user1, USER, project); - db.users().insertProjectPermissionOnUser(user2, ADMIN, project); - - indexOnStartup(); - - // anonymous - verifyAnyoneNotAuthorized(project); - - // user1 has access - verifyAuthorized(project, user1); - - // user2 has not access (only USER permission is accepted) - verifyNotAuthorized(project, user2); - } - - @Test - public void indexOnStartup_grants_access_to_group_on_private_project() { - ProjectDto project = createAndIndexPrivateProject(); - UserDto user1 = db.users().insertUser(); - UserDto user2 = db.users().insertUser(); - UserDto user3 = db.users().insertUser(); - GroupDto group1 = db.users().insertGroup(); - GroupDto group2 = db.users().insertGroup(); - db.users().insertEntityPermissionOnGroup(group1, USER, project); - db.users().insertEntityPermissionOnGroup(group2, ADMIN, project); - - indexOnStartup(); - - // anonymous - verifyAnyoneNotAuthorized(project); - - // group1 has access - verifyAuthorized(project, user1, group1); - - // group2 has not access (only USER permission is accepted) - verifyNotAuthorized(project, user2, group2); - - // user3 is not in any group - verifyNotAuthorized(project, user3); - } - - @Test - public void indexOnStartup_grants_access_to_user_and_group() { - ProjectDto project = createAndIndexPrivateProject(); - UserDto user1 = db.users().insertUser(); - UserDto user2 = db.users().insertUser(); - GroupDto group = db.users().insertGroup(); - db.users().insertMember(group, user2); - db.users().insertProjectPermissionOnUser(user1, USER, project); - db.users().insertEntityPermissionOnGroup(group, USER, project); - - indexOnStartup(); - - // anonymous - verifyAnyoneNotAuthorized(project); - - // has direct access - verifyAuthorized(project, user1); - - // has access through group - verifyAuthorized(project, user1, group); - - // no access - verifyNotAuthorized(project, user2); - } - - @Test - public void indexOnStartup_does_not_grant_access_to_anybody_on_private_project() { - ProjectDto project = createAndIndexPrivateProject(); - UserDto user = db.users().insertUser(); - GroupDto group = db.users().insertGroup(); - - indexOnStartup(); - - verifyAnyoneNotAuthorized(project); - verifyNotAuthorized(project, user); - verifyNotAuthorized(project, user, group); - } - - @Test - public void indexOnStartup_grants_access_to_anybody_on_public_project() { - ProjectDto project = createAndIndexPublicProject(); - UserDto user = db.users().insertUser(); - GroupDto group = db.users().insertGroup(); - - indexOnStartup(); - - verifyAnyoneAuthorized(project); - verifyAuthorized(project, user); - verifyAuthorized(project, user, group); - } - - @Test - public void indexOnStartup_grants_access_to_anybody_on_view() { - PortfolioDto view = createAndIndexPortfolio(); - UserDto user = db.users().insertUser(); - GroupDto group = db.users().insertGroup(); - - indexOnStartup(); - - verifyAnyoneAuthorized(view); - verifyAuthorized(view, user); - verifyAuthorized(view, user, group); - } - - @Test - public void indexOnStartup_grants_access_on_many_projects() { - UserDto user1 = db.users().insertUser(); - UserDto user2 = db.users().insertUser(); - ProjectDto project = null; - for (int i = 0; i < 10; i++) { - project = createAndIndexPrivateProject(); - db.users().insertProjectPermissionOnUser(user1, USER, project); - } - - indexOnStartup(); - - verifyAnyoneNotAuthorized(project); - verifyAuthorized(project, user1); - verifyNotAuthorized(project, user2); - } - - @Test - public void public_projects_are_visible_to_anybody() { - ProjectDto projectOnOrg1 = createAndIndexPublicProject(); - UserDto user = db.users().insertUser(); - - indexOnStartup(); - - verifyAnyoneAuthorized(projectOnOrg1); - verifyAuthorized(projectOnOrg1, user); - } - - @Test - public void permissions_are_not_updated_on_project_tags_update() { - ProjectDto project = createAndIndexPublicProject(); - - indexPermissions(project, EntityEvent.PROJECT_TAGS_UPDATE); - - assertThatAuthIndexHasSize(0); - verifyAnyoneNotAuthorized(project); - } - - @Test - public void permissions_are_not_updated_on_project_key_update() { - ProjectDto project = createAndIndexPublicProject(); - - indexPermissions(project, EntityEvent.PROJECT_TAGS_UPDATE); - - assertThatAuthIndexHasSize(0); - verifyAnyoneNotAuthorized(project); - } - - @Test - public void index_permissions_on_project_creation() { - ProjectDto project = createAndIndexPrivateProject(); - UserDto user = db.users().insertUser(); - db.users().insertProjectPermissionOnUser(user, USER, project); - - indexPermissions(project, EntityEvent.CREATION); - - assertThatAuthIndexHasSize(1); - verifyAuthorized(project, user); - } - - @Test - public void index_permissions_on_permission_change() { - ProjectDto project = createAndIndexPrivateProject(); - UserDto user1 = db.users().insertUser(); - UserDto user2 = db.users().insertUser(); - db.users().insertProjectPermissionOnUser(user1, USER, project); - indexPermissions(project, EntityEvent.CREATION); - verifyAuthorized(project, user1); - verifyNotAuthorized(project, user2); - - db.users().insertProjectPermissionOnUser(user2, USER, project); - indexPermissions(project, PERMISSION_CHANGE); - - verifyAuthorized(project, user1); - verifyAuthorized(project, user1); - } - - @Test - public void delete_permissions_on_project_deletion() { - ProjectDto project = createAndIndexPrivateProject(); - UserDto user = db.users().insertUser(); - db.users().insertProjectPermissionOnUser(user, USER, project); - indexPermissions(project, EntityEvent.CREATION); - verifyAuthorized(project, user); - - db.getDbClient().purgeDao().deleteProject(db.getSession(), project.getUuid(), PROJECT, project.getUuid(), project.getKey()); - indexPermissions(project, EntityEvent.DELETION); - - verifyNotAuthorized(project, user); - assertThatAuthIndexHasSize(0); - } - - @Test - public void errors_during_indexing_are_recovered() { - ProjectDto project = createAndIndexPublicProject(); - es.lockWrites(INDEX_TYPE_FOO_AUTH); - - IndexingResult result = indexPermissions(project, PERMISSION_CHANGE); - assertThat(result.getTotal()).isOne(); - assertThat(result.getFailures()).isOne(); - - // index is still read-only, fail to recover - result = recover(); - assertThat(result.getTotal()).isOne(); - assertThat(result.getFailures()).isOne(); - assertThatAuthIndexHasSize(0); - assertThatEsQueueTableHasSize(1); - - es.unlockWrites(INDEX_TYPE_FOO_AUTH); - - result = recover(); - assertThat(result.getTotal()).isOne(); - assertThat(result.getFailures()).isZero(); - verifyAnyoneAuthorized(project); - assertThatEsQueueTableHasSize(0); - } - - private void assertThatAuthIndexHasSize(int expectedSize) { - assertThat(es.countDocuments(FooIndexDefinition.TYPE_AUTHORIZATION)).isEqualTo(expectedSize); - } - - private void indexOnStartup() { - underTest.indexOnStartup(underTest.getIndexTypes()); - } - - private void verifyAuthorized(EntityDto entity, UserDto user) { - logIn(user); - verifyAuthorized(entity, true); - } - - private void verifyAuthorized(EntityDto entity, UserDto user, GroupDto group) { - logIn(user).setGroups(group); - verifyAuthorized(entity, true); - } - - private void verifyNotAuthorized(EntityDto entity, UserDto user) { - logIn(user); - verifyAuthorized(entity, false); - } - - private void verifyNotAuthorized(EntityDto entity, UserDto user, GroupDto group) { - logIn(user).setGroups(group); - verifyAuthorized(entity, false); - } - - private void verifyAnyoneAuthorized(EntityDto entity) { - userSession.anonymous(); - verifyAuthorized(entity, true); - } - - private void verifyAnyoneNotAuthorized(EntityDto entity) { - userSession.anonymous(); - verifyAuthorized(entity, false); - } - - private void verifyAuthorized(EntityDto entity, boolean expectedAccess) { - assertThat(fooIndex.hasAccessToProject(entity.getUuid())).isEqualTo(expectedAccess); - } - - private UserSessionRule logIn(UserDto u) { - userSession.logIn(u); - return userSession; - } - - private IndexingResult indexPermissions(EntityDto entity, EntityEvent cause) { - DbSession dbSession = db.getSession(); - Collection items = underTest.prepareForRecoveryOnEntityEvent(dbSession, singletonList(entity.getUuid()), cause); - dbSession.commit(); - return underTest.index(dbSession, items); - } - - private ProjectDto createUnindexedPublicProject() { - return db.components().insertPublicProject().getProjectDto(); - } - - private ProjectDto createAndIndexPrivateProject() { - ProjectData project = db.components().insertPrivateProject(); - fooIndexer.indexOnAnalysis(project.getMainBranchDto().getUuid()); - return project.getProjectDto(); - } - - private ProjectDto createAndIndexPublicProject() { - ProjectData project = db.components().insertPublicProject(); - fooIndexer.indexOnAnalysis(project.getMainBranchDto().getUuid()); - return project.getProjectDto(); - } - - private PortfolioDto createAndIndexPortfolio() { - PortfolioDto view = db.components().insertPublicPortfolioDto(); - fooIndexer.indexOnAnalysis(view.getUuid()); - return view; - } - - private IndexingResult recover() { - Collection items = db.getDbClient().esQueueDao().selectForRecovery(db.getSession(), System.currentTimeMillis() + 1_000L, 10); - return underTest.index(db.getSession(), items); - } - - private void assertThatEsQueueTableHasSize(int expectedSize) { - assertThat(db.countRowsOfTable("es_queue")).isEqualTo(expectedSize); - } - -} diff --git a/server/sonar-webserver-es/src/testFixtures/java/org/sonar/server/permission/index/FooIndex.java b/server/sonar-webserver-es/src/testFixtures/java/org/sonar/server/permission/index/FooIndex.java new file mode 100644 index 00000000000..ab0ff8755eb --- /dev/null +++ b/server/sonar-webserver-es/src/testFixtures/java/org/sonar/server/permission/index/FooIndex.java @@ -0,0 +1,52 @@ +/* + * 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.permission.index; + +import java.util.Arrays; +import java.util.List; +import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.search.SearchHits; +import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.sonar.server.es.EsClient; + +import static org.sonar.server.permission.index.FooIndexDefinition.DESCRIPTOR; + +public class FooIndex { + + private final EsClient esClient; + private final WebAuthorizationTypeSupport authorizationTypeSupport; + + public FooIndex(EsClient esClient, WebAuthorizationTypeSupport authorizationTypeSupport) { + this.esClient = esClient; + this.authorizationTypeSupport = authorizationTypeSupport; + } + + public boolean hasAccessToProject(String projectUuid) { + SearchHits hits = esClient.search(EsClient.prepareSearch(DESCRIPTOR.getName()) + .source(new SearchSourceBuilder().query(QueryBuilders.boolQuery() + .must(QueryBuilders.termQuery(FooIndexDefinition.FIELD_PROJECT_UUID, projectUuid)) + .filter(authorizationTypeSupport.createQueryFilter())))) + .getHits(); + List names = Arrays.stream(hits.getHits()) + .map(h -> h.getSourceAsMap().get(FooIndexDefinition.FIELD_NAME).toString()) + .toList(); + return names.size() == 2 && names.contains("bar") && names.contains("baz"); + } +} diff --git a/server/sonar-webserver-es/src/testFixtures/java/org/sonar/server/permission/index/FooIndexer.java b/server/sonar-webserver-es/src/testFixtures/java/org/sonar/server/permission/index/FooIndexer.java new file mode 100644 index 00000000000..cd8e1c4c262 --- /dev/null +++ b/server/sonar-webserver-es/src/testFixtures/java/org/sonar/server/permission/index/FooIndexer.java @@ -0,0 +1,101 @@ +/* + * 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.permission.index; + +import java.util.Optional; +import java.util.Set; +import org.elasticsearch.action.index.IndexRequest; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.component.BranchDto; +import org.sonar.server.es.AnalysisIndexer; +import org.sonar.server.es.BaseDoc; +import org.sonar.server.es.EsClient; + +import static org.sonar.server.permission.index.FooIndexDefinition.TYPE_FOO; + +public class FooIndexer implements AnalysisIndexer, NeedAuthorizationIndexer { + + private static final AuthorizationScope AUTHORIZATION_SCOPE = new AuthorizationScope(TYPE_FOO, p -> true); + + private final EsClient esClient; + private final DbClient dbClient; + + public FooIndexer(EsClient esClient, DbClient dbClient) { + this.esClient = esClient; + this.dbClient = dbClient; + } + + @Override + public AuthorizationScope getAuthorizationScope() { + return AUTHORIZATION_SCOPE; + } + + @Override + public void indexOnAnalysis(String branchUuid) { + indexOnAnalysis(branchUuid, Set.of()); + } + + @Override + public void indexOnAnalysis(String branchUuid, Set unchangedComponentUuids) { + try(DbSession dbSession = dbClient.openSession(true)){ + Optional branchDto = dbClient.branchDao().selectByUuid(dbSession, branchUuid); + if (branchDto.isEmpty()) { + //For portfolio, adding branchUuid directly + addToIndex(branchUuid, "bar"); + addToIndex(branchUuid, "baz"); + }else{ + addToIndex(branchDto.get().getProjectUuid(), "bar"); + addToIndex(branchDto.get().getProjectUuid(), "baz"); + } + } + + + } + + private void addToIndex(String projectUuid, String name) { + FooDoc fooDoc = new FooDoc(projectUuid, name); + esClient.index(new IndexRequest(TYPE_FOO.getMainType().getIndex().getName()) + .type(TYPE_FOO.getMainType().getType()) + .id(fooDoc.getId()) + .routing(fooDoc.getRouting().orElse(null)) + .source(fooDoc.getFields())); + } + + private static final class FooDoc extends BaseDoc { + private final String projectUuid; + private final String name; + + private FooDoc(String projectUuid, String name) { + super(TYPE_FOO); + this.projectUuid = projectUuid; + this.name = name; + setField(FooIndexDefinition.FIELD_PROJECT_UUID, projectUuid); + setField(FooIndexDefinition.FIELD_NAME, name); + setParent(AuthorizationDoc.idOf(projectUuid)); + } + + @Override + public String getId() { + return projectUuid + "_" + name; + } + + } +}