CVE-2024-32466: Insufficient Information (NVD-CWE-noinfo)
value: Tolgee is an open-source localization platform. For the `/v2/projects/translations` and `/v2/projects/{projectId}/translations` endpoints, translation data was returned even when API key was missing `translation.view` scope. However, it was impossible to fetch the data when user was missing this scope. So this is only relevant for API keys generated by users permitted to `translation.view`. This vulnerability is fixed in v3.57.2

CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-04-18T15:15Z
Repository: https://github.com/tolgee/tolgee-platform
Language: Kotlin
Files changed: backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/ApiKeyController.kt, backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/translation/TranslationsController.kt, backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/V2InvitationControllerTest.kt, backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/translations/v2TranslationsController/TranslationsControllerViewTest.kt, backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ProjectsController/ProjectsControllerPermissionsTest.kt, backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ProjectsController/ProjectsControllerTest.kt, backend/data/src/main/kotlin/io/tolgee/constants/Message.kt, backend/data/src/main/kotlin/io/tolgee/model/enums/Scope.kt, backend/data/src/main/kotlin/io/tolgee/service/LanguageService.kt, backend/data/src/main/kotlin/io/tolgee/service/security/ApiKeyService.kt, backend/data/src/main/kotlin/io/tolgee/service/security/PermissionService.kt, backend/data/src/main/kotlin/io/tolgee/service/security/SecurityService.kt, backend/security/src/main/kotlin/io/tolgee/security/authorization/ProjectAuthorizationInterceptor.kt, backend/security/src/test/kotlin/io/tolgee/security/authorization/ProjectAuthorizationInterceptorTest.kt, backend/testing/src/main/kotlin/io/tolgee/fixtures/ProjectApiKeyAuthRequestPerformer.kt, e2e/cypress/e2e/projects/export/export.cy.ts
Commit date: 2024-04-17 14:26:51 +0200
Commit message: fix: Translation endpoints permission check (#2254)


Diff: diff --git a/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/ApiKeyController.kt b/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/ApiKeyController.kt
index b8dbdb67b..d663f87f7 100644
--- a/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/ApiKeyController.kt
+++ b/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/ApiKeyController.kt
@@ -163,23 +163,18 @@ class ApiKeyController(
         projectIdNotNull,
         authenticationFacade.authenticatedUser.id,
       )
 
     val computed = permissionData.computedPermissions
-    val scopes =
-      when {
-        apiKeyAuthentication -> authenticationFacade.projectApiKey.scopes.toTypedArray()
-        else -> computed.scopes
-      }
 
     return ApiKeyPermissionsModel(
       projectIdNotNull,
       type = if (apiKeyAuthentication) null else computed.type,
       translateLanguageIds = computed.translateLanguageIds.toNormalizedPermittedLanguageSet(),
       viewLanguageIds = computed.viewLanguageIds.toNormalizedPermittedLanguageSet(),
       stateChangeLanguageIds = computed.stateChangeLanguageIds.toNormalizedPermittedLanguageSet(),
-      scopes = scopes,
+      scopes = securityService.getCurrentPermittedScopes(projectIdNotNull).toTypedArray(),
       project = simpleProjectModelAssembler.toModel(projectService.get(projectIdNotNull)),
     )
   }
 
   fun Set<Long>?.toNormalizedPermittedLanguageSet(): Set<Long>? {
diff --git a/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/translation/TranslationsController.kt b/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/translation/TranslationsController.kt
index 6858a7d27..9bb475651 100644
--- a/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/translation/TranslationsController.kt
+++ b/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/translation/TranslationsController.kt
@@ -26,11 +26,10 @@ import io.tolgee.hateoas.translations.KeysWithTranslationsPagedResourcesAssemble
 import io.tolgee.hateoas.translations.SetTranslationsResponseModel
 import io.tolgee.hateoas.translations.TranslationHistoryModel
 import io.tolgee.hateoas.translations.TranslationHistoryModelAssembler
 import io.tolgee.hateoas.translations.TranslationModel
 import io.tolgee.hateoas.translations.TranslationModelAssembler
-import io.tolgee.model.Screenshot
 import io.tolgee.model.enums.AssignableTranslationState
 import io.tolgee.model.enums.Scope
 import io.tolgee.model.translation.Translation
 import io.tolgee.model.views.KeyWithTranslationsView
 import io.tolgee.openApiDocs.OpenApiOrderExtension
@@ -45,10 +44,11 @@ import io.tolgee.service.queryBuilders.CursorUtil
 import io.tolgee.service.security.SecurityService
 import io.tolgee.service.translation.TranslationService
 import jakarta.validation.Valid
 import org.springdoc.core.annotations.ParameterObject
 import org.springframework.beans.propertyeditors.CustomCollectionEditor
+import org.springframework.data.domain.Page
 import org.springframework.data.domain.PageRequest
 import org.springframework.data.domain.Pageable
 import org.springframework.data.domain.Sort
 import org.springframework.data.web.PagedResourcesAssembler
 import org.springframework.data.web.SortDefault
@@ -227,11 +227,11 @@ When null, resulting file will be a flat key-value object.
     )
   }
 
   @GetMapping(value = [""])
   @Operation(summary = "Get translations in project")
-  @UseDefaultPermissions // Security: check done internally
+  @RequiresProjectPermissions(scopes = [Scope.KEYS_VIEW]) // Security: check done internally
   @AllowApiAccess
   @Transactional
   @OpenApiOrderExtension(5)
   fun getTranslations(
     @ParameterObject
@@ -250,20 +250,28 @@ When null, resulting file will be a flat key-value object.
 
     val data =
       translationService
         .getViewData(projectHolder.project.id, pageableWithSort, params, languages)
 
-    val keysWithScreenshots = getScreenshots(data.map { it.keyId }.toList())
-
-    if (keysWithScreenshots != null) {
-      data.content.forEach { it.screenshots = keysWithScreenshots[it.keyId] ?: listOf() }
-    }
+    addScreenshotsToResponse(data)
 
     val cursor = if (data.content.isNotEmpty()) CursorUtil.getCursor(data.content.last(), data.sort) else null
     return pagedAssembler.toTranslationModel(data, languages, cursor)
   }
 
+  private fun addScreenshotsToResponse(data: Page<KeyWithTranslationsView>) {
+    val canViewScreenshots = securityService.currentPermittedScopesContain(Scope.SCREENSHOTS_VIEW)
+
+    if (!canViewScreenshots) {
+      return
+    }
+
+    val keysWithScreenshots = screenshotService.getScreenshotsForKeys(data.map { it.keyId }.content)
+
+    data.content.forEach { it.screenshots = keysWithScreenshots[it.keyId] ?: listOf() }
+  }
+
   @PutMapping(value = ["/{translationId:[0-9]+}/dismiss-auto-translated-state"])
   @Operation(summary = "Dismiss auto-translated", description = """Removes "auto translated" indication""")
   @RequestActivity(ActivityType.DISMISS_AUTO_TRANSLATED_STATE)
   @RequiresProjectPermissions([Scope.TRANSLATIONS_STATE_EDIT])
   @AllowApiAccess
@@ -315,27 +323,10 @@ When null, resulting file will be a flat key-value object.
     securityService.checkLanguageViewPermission(projectHolder.project.id, listOf(translation.language.id))
     val translations = activityService.getTranslationHistory(translation.id, pageable)
     return historyPagedAssembler.toModel(translations, historyModelAssembler)
   }
 
-  private fun getScreenshots(keyIds: Collection<Long>): Map<Long, List<Screenshot>>? {
-    if (
-      !authenticationFacade.isProjectApiKeyAuth ||
-      authenticationFacade.projectApiKey.scopes.contains(Scope.SCREENSHOTS_VIEW)
-    ) {
-      return screenshotService.getScreenshotsForKeys(keyIds)
-    }
-    return null
-  }
-
-  private fun checkKeyEditScope() {
-    securityService.checkProjectPermission(
-      projectHolder.project.id,
-      Scope.KEYS_EDIT,
-    )
-  }
-
   private fun getSafeSortPageable(pageable: Pageable): Pageable {
     var sort = pageable.sort
     if (sort.getOrderFor(KeyWithTranslationsView::keyId.name) == null) {
       sort = sort.and(Sort.by(Sort.Direction.ASC, KeyWithTranslationsView::keyId.name))
     }
diff --git a/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/V2InvitationControllerTest.kt b/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/V2InvitationControllerTest.kt
index 4466bfc3d..142a67055 100644
--- a/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/V2InvitationControllerTest.kt
+++ b/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/V2InvitationControllerTest.kt
@@ -109,12 +109,12 @@ class V2InvitationControllerTest : AuthorizedControllerTest() {
     project: Project,
     newUser: UserAccount,
     expectedType: ProjectPermissionType,
   ) {
     assertThat(invitationService.getForProject(project)).hasSize(0)
-    assertThat(permissionService.getProjectPermissionScopes(project.id, newUser)).isNotNull
-    val type = permissionService.getProjectPermissionScopes(project.id, newUser)!!
+    assertThat(permissionService.getProjectPermissionScopesNoApiKey(project.id, newUser)).isNotNull
+    val type = permissionService.getProjectPermissionScopesNoApiKey(project.id, newUser)!!
     type.assert.equalsPermissionType(expectedType)
   }
 
   private fun createTranslateInvitation(project: Project): Invitation {
     return invitationService.create(
diff --git a/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/translations/v2TranslationsController/TranslationsControllerViewTest.kt b/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/translations/v2TranslationsController/TranslationsControllerViewTest.kt
index 7b748176e..c605fdeb8 100644
--- a/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/translations/v2TranslationsController/TranslationsControllerViewTest.kt
+++ b/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/translations/v2TranslationsController/TranslationsControllerViewTest.kt
@@ -2,10 +2,11 @@ package io.tolgee.api.v2.controllers.translations.v2TranslationsController
 
 import io.tolgee.ProjectAuthControllerTest
 import io.tolgee.development.testDataBuilder.data.NamespacesTestData
 import io.tolgee.development.testDataBuilder.data.TranslationsTestData
 import io.tolgee.fixtures.andAssertThatJson
+import io.tolgee.fixtures.andIsForbidden
 import io.tolgee.fixtures.andIsNotFound
 import io.tolgee.fixtures.andIsOk
 import io.tolgee.fixtures.andPrettyPrint
 import io.tolgee.fixtures.isValidId
 import io.tolgee.fixtures.node
@@ -111,10 +112,21 @@ class TranslationsControllerViewTest : ProjectAuthControllerTest("/v2/projects/"
     performProjectAuthGet("/translations?sort=id").andIsOk.andAssertThatJson {
       node("_embedded.keys[2].translations.de").isAbsent()
     }
   }
 
+  @Test
+  @ProjectApiKeyAuthTestMethod(scopes = [Scope.KEYS_VIEW])
+  fun `returns empty translations when api key is missing the translations-view scope`() {
+    testData.addKeysViewOnlyUser()
+    testDataService.saveTestData(testData.root)
+    userAccount = testData.user
+    performProjectAuthGet("/translations?sort=id").andIsOk.andAssertThatJson {
+      node("_embedded.keys[2].translations.de").isAbsent()
+    }
+  }
+
   @Test
   @ProjectJWTAuthTestMethod
   fun `returns correct screenshot data`() {
     testData.addKeysWithScreenshots()
     testDataService.saveTestData(testData.root)
@@ -237,10 +249,30 @@ class TranslationsControllerViewTest : ProjectAuthControllerTest("/v2/projects/"
       .andAssertThatJson {
         node("page.totalElements").isEqualTo(2)
       }
   }
 
+  @ProjectApiKeyAuthTestMethod(scopes = [Scope.BATCH_JOBS_VIEW])
+  @Test
+  fun `checks keys view scope (missing scope)`() {
+    testDataService.saveTestData(testData.root)
+    userAccount = testData.user
+    performProjectAuthGet("/translations").andIsForbidden
+  }
+
+  @ProjectApiKeyAuthTestMethod(scopes = [Scope.TRANSLATIONS_VIEW])
+  @Test
+  fun `returns no screenshots when screenshot view scope is missing`() {
+    testData.addKeysWithScreenshots()
+    testDataService.saveTestData(testData.root)
+    userAccount = testData.user
+    performProjectAuthGet("/translations").andPrettyPrint.andAssertThatJson {
+      node("_embedded.keys[3].screenshots").isNull()
+      node("_embedded.keys[3].screenshotCount").isEqualTo(2)
+    }
+  }
+
   @ProjectApiKeyAuthTestMethod(apiKeyPresentType = ApiKeyPresentMode.QUERY_PARAM)
   @Test
   fun `works with API key in query`() {
     testDataService.saveTestData(testData.root)
     userAccount = testData.user
diff --git a/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ProjectsController/ProjectsControllerPermissionsTest.kt b/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ProjectsController/ProjectsControllerPermissionsTest.kt
index 0b811b543..a76c4bef3 100644
--- a/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ProjectsController/ProjectsControllerPermissionsTest.kt
+++ b/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ProjectsController/ProjectsControllerPermissionsTest.kt
@@ -25,11 +25,11 @@ class ProjectsControllerPermissionsTest : ProjectAuthControllerTest("/v2/project
   @Test
   fun `sets user permissions`() {
     permissionTestUtil.withPermissionsTestData { project, user ->
       performAuthPut("/v2/projects/${project.id}/users/${user.id}/set-permissions/EDIT", null).andIsOk
 
-      permissionService.getProjectPermissionScopes(project.id, user)
+      permissionService.getProjectPermissionScopesNoApiKey(project.id, user)
         .let { Assertions.assertThat(it).equalsPermissionType(ProjectPermissionType.EDIT) }
     }
   }
 
   @Test
diff --git a/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ProjectsController/ProjectsControllerTest.kt b/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ProjectsController/ProjectsControllerTest.kt
index 62be1d1f0..fb0edd44a 100644
--- a/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ProjectsController/ProjectsControllerTest.kt
+++ b/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ProjectsController/ProjectsControllerTest.kt
@@ -20,11 +20,11 @@ import org.junit.jupiter.api.Test
 import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
 import org.springframework.boot.test.context.SpringBootTest
 
 @SpringBootTest
 @AutoConfigureMockMvc
-open class ProjectsControllerTest : ProjectAuthControllerTest("/v2/projects/") {
+class ProjectsControllerTest : ProjectAuthControllerTest("/v2/projects/") {
   @Test
   fun getAll() {
     executeInNewTransaction {
       dbPopulator.createBase("one", "kim")
       dbPopulator.createBase("two", "kim")
@@ -240,11 +240,11 @@ open class ProjectsControllerTest : ProjectAuthControllerTest("/v2/projects/") {
 
     loginAsUser(usersAndOrganizations[1].name)
 
     performAuthPut("/v2/projects/${repo.id}/users/${user.id}/revoke-access", null).andIsOk
 
-    permissionService.getProjectPermissionScopes(repo.id, user)
+    permissionService.getProjectPermissionScopesNoApiKey(repo.id, user)
       .let { assertThat(it).isEmpty() }
   }
 
   @Test
   fun revokeUsersAccessOwn() {
diff --git a/backend/data/src/main/kotlin/io/tolgee/constants/Message.kt b/backend/data/src/main/kotlin/io/tolgee/constants/Message.kt
index f1a43d0ff..ecf5c2011 100644
--- a/backend/data/src/main/kotlin/io/tolgee/constants/Message.kt
+++ b/backend/data/src/main/kotlin/io/tolgee/constants/Message.kt
@@ -205,10 +205,11 @@ enum class Message {
   UNSUPPORTED_PO_MESSAGE_FORMAT,
   PLURAL_FORMS_DATA_LOSS,
   CURRENT_USER_DOES_NOT_OWN_IMAGE,
   USER_CANNOT_VIEW_THIS_ORGANIZATION,
   USER_IS_NOT_OWNER_OF_ORGANIZATION,
+  PAK_CREATED_FOR_DIFFERENT_PROJECT,
   ;
 
   val code: String
     get() = name.lowercase(Locale.getDefault())
 }
diff --git a/backend/data/src/main/kotlin/io/tolgee/model/enums/Scope.kt b/backend/data/src/main/kotlin/io/tolgee/model/enums/Scope.kt
index acc7ed18c..500ce29f4 100644
--- a/backend/data/src/main/kotlin/io/tolgee/model/enums/Scope.kt
+++ b/backend/data/src/main/kotlin/io/tolgee/model/enums/Scope.kt
@@ -140,21 +140,22 @@ enum class Scope(
       return getScopeHierarchyItems(root = hierarchy, scope).toTypedArray()
     }
 
     fun expand(scope: Scope): Array<Scope> {
       val hierarchyItems = getScopeHierarchyItems(scope)
-      return hierarchyItems.flatMap { expand(it) }.toTypedArray()
+      return hierarchyItems.flatMap { expand(it) }.toSet().toTypedArray()
     }
 
     /**
      * Returns all scopes recursively
      *
      * Example: When permittedScopes == [ADMIN], it returns all the scopes which is included in
      * ADMIN scope, (TRANSLATION_VIEW, KEYS_EDIT, etc.)
      *
      */
-    fun expand(permittedScopes: Array<Scope>): Array<Scope> {
+    fun expand(permittedScopes: Array<Scope>?): Array<Scope> {
+      permittedScopes ?: return arrayOf()
       return permittedScopes.flatMap { expand(it).toList() }.toSet().toTypedArray()
     }
 
     fun expand(permittedScopes: Collection<Scope>): Array<Scope> {
       return expand(permittedScopes.toTypedArray())
diff --git a/backend/data/src/main/kotlin/io/tolgee/service/LanguageService.kt b/backend/data/src/main/kotlin/io/tolgee/service/LanguageService.kt
index 6c6f06ee0..e5a2c1009 100644
--- a/backend/data/src/main/kotlin/io/tolgee/service/LanguageService.kt
+++ b/backend/data/src/main/kotlin/io/tolgee/service/LanguageService.kt
@@ -221,12 +221,11 @@ class LanguageService(
   fun getLanguagesForTranslationsView(
     languages: Set<String>?,
     projectId: Long,
     userId: Long,
   ): Set<LanguageDto> {
-    val canViewTranslations =
-      permissionService.getProjectPermissionScopes(projectId, userId)?.contains(Scope.TRANSLATIONS_VIEW) == true
+    val canViewTranslations = securityService.currentPermittedScopesContain(Scope.TRANSLATIONS_VIEW)
 
     if (!canViewTranslations) {
       return emptySet()
     }
     return if (languages == null) {
diff --git a/backend/data/src/main/kotlin/io/tolgee/service/security/ApiKeyService.kt b/backend/data/src/main/kotlin/io/tolgee/service/security/ApiKeyService.kt
index cbf14be86..93e5a4038 100644
--- a/backend/data/src/main/kotlin/io/tolgee/service/security/ApiKeyService.kt
+++ b/backend/data/src/main/kotlin/io/tolgee/service/security/ApiKeyService.kt
@@ -126,11 +126,11 @@ class ApiKeyService(
   fun getAvailableScopes(
     userAccountId: Long,
     project: Project,
   ): Array<Scope> {
     val permittedScopes =
-      permissionService.getProjectPermissionScopes(project.id, userAccountId)
+      permissionService.getProjectPermissionScopesNoApiKey(project.id, userAccountId)
         ?: throw NotFoundException()
     return Scope.expand(permittedScopes)
   }
 
   @CacheEvict(cacheNames = [Caches.PROJECT_API_KEYS], key = "#apiKey.keyHash")
diff --git a/backend/data/src/main/kotlin/io/tolgee/service/security/PermissionService.kt b/backend/data/src/main/kotlin/io/tolgee/service/security/PermissionService.kt
index c5f78c8e5..16b1562a6 100644
--- a/backend/data/src/main/kotlin/io/tolgee/service/security/PermissionService.kt
+++ b/backend/data/src/main/kotlin/io/tolgee/service/security/PermissionService.kt
@@ -65,16 +65,16 @@ class PermissionService(
 
   fun findById(id: Long): Permission? {
     return cachedPermissionService.find(id)
   }
 
-  fun getProjectPermissionScopes(
+  fun getProjectPermissionScopesNoApiKey(
     projectId: Long,
     userAccount: UserAccount,
-  ) = getProjectPermissionScopes(projectId, userAccount.id)
+  ) = getProjectPermissionScopesNoApiKey(projectId, userAccount.id)
 
-  fun getProjectPermissionScopes(
+  fun getProjectPermissionScopesNoApiKey(
     projectId: Long,
     userAccountId: Long,
   ): Array<Scope>? {
     return getProjectPermissionData(projectId, userAccountId).computedPermissions.expandedScopes
   }
diff --git a/backend/data/src/main/kotlin/io/tolgee/service/security/SecurityService.kt b/backend/data/src/main/kotlin/io/tolgee/service/security/SecurityService.kt
index b5e5632c4..e48f660b7 100644
--- a/backend/data/src/main/kotlin/io/tolgee/service/security/SecurityService.kt
+++ b/backend/data/src/main/kotlin/io/tolgee/service/security/SecurityService.kt
@@ -5,26 +5,27 @@ import io.tolgee.dtos.ComputedPermissionDto
 import io.tolgee.dtos.cacheable.ApiKeyDto
 import io.tolgee.dtos.cacheable.UserAccountDto
 import io.tolgee.exceptions.LanguageNotPermittedException
 import io.tolgee.exceptions.NotFoundException
 import io.tolgee.exceptions.PermissionException
-import io.tolgee.model.ApiKey
 import io.tolgee.model.Project
 import io.tolgee.model.UserAccount
 import io.tolgee.model.enums.Scope
 import io.tolgee.model.translation.Translation
 import io.tolgee.repository.KeyRepository
+import io.tolgee.security.ProjectHolder
 import io.tolgee.security.authentication.AuthenticationFacade
 import io.tolgee.service.LanguageService
 import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.stereotype.Service
 
 @Service
 class SecurityService(
   private val authenticationFacade: AuthenticationFacade,
   private val languageService: LanguageService,
   private val keyRepository: KeyRepository,
+  private val projectHolder: ProjectHolder,
 ) {
   @set:Autowired
   lateinit var apiKeyService: ApiKeyService
 
   @set:Autowired
@@ -33,17 +34,51 @@ class SecurityService(
   @set:Autowired
   lateinit var userAccountService: UserAccountService
 
   fun checkAnyProjectPermission(projectId: Long) {
     if (
-      getProjectPermissionScopes(projectId).isNullOrEmpty() &&
+      getProjectPermissionScopesNoApiKey(projectId).isNullOrEmpty() &&
       !isCurrentUserServerAdmin()
     ) {
       throw PermissionException(Message.USER_HAS_NO_PROJECT_ACCESS)
     }
   }
 
+  fun currentPermittedScopesContain(scope: Scope): Boolean {
+    return currentPermittedScopesContain(listOf(scope))
+  }
+
+  fun currentPermittedScopesContain(scopes: Collection<Scope>?): Boolean {
+    scopes ?: return true
+    return getCurrentPermittedScopes(projectHolder.project.id).containsAll(scopes)
+  }
+
+  /**
+   * Returns current permitted scopes, expanded
+   */
+  fun getCurrentPermittedScopes(projectId: Long): Set<Scope> {
+    val projectScopes =
+      Scope.expand(
+        getProjectPermissionScopesNoApiKey(projectId, authenticationFacade.authenticatedUser.id),
+      ).toSet()
+    val apiKey = activeApiKey ?: return projectScopes
+
+    return Scope.expand(apiKey.scopes).toSet().intersect(projectScopes.toSet())
+  }
+
+  fun checkProjectPermission(
+    projectId: Long,
+    requiredPermission: Scope,
+  ) {
+    // Always check for the current user even if we're using an API key for security reasons.
+    // This prevents improper preservation of permissions.
+    checkProjectPermissionNoApiKey(projectId, requiredPermission, activeUser)
+
+    val apiKey = activeApiKey ?: return
+    checkProjectPermission(projectId, requiredPermission, apiKey)
+  }
+
   fun checkProjectPermission(
     projectId: Long,
     requiredScopes: Scope,
     apiKey: ApiKeyDto,
   ) {
@@ -65,11 +100,11 @@ class SecurityService(
     if (isUserAdmin(userAccountDto)) {
       return
     }
 
     val allowedScopes =
-      getProjectPermissionScopes(projectId, userAccountDto.id)
+      getProjectPermissionScopesNoApiKey(projectId, userAccountDto.id)
         ?: throw PermissionException(Message.USER_HAS_NO_PROJECT_ACCESS)
 
     checkPermission(requiredScope, allowedScopes)
   }
 
@@ -83,22 +118,10 @@ class SecurityService(
         listOf(requiredScope),
       )
     }
   }
 
-  fun checkProjectPermission(
-    projectId: Long,
-    requiredPermission: Scope,
-  ) {
-    // Always check for the current user even if we're using an API key for security reasons.
-    // This prevents improper preservation of permissions.
-    checkProjectPermissionNoApiKey(projectId, requiredPermission, activeUser)
-
-    val apiKey = activeApiKey ?: return
-    checkProjectPermission(projectId, requiredPermission, apiKey)
-  }
-
   fun checkLanguageViewPermissionByTag(
     projectId: Long,
     languageTags: Collection<String>,
   ) {
     checkProjectPermission(projectId, Scope.TRANSLATIONS_VIEW)
@@ -267,23 +290,10 @@ class SecurityService(
 
   fun checkBigMetaUploadPermission(projectId: Long) {
     checkProjectPermission(projectId, Scope.TRANSLATIONS_EDIT)
   }
 
-  fun fixInvalidApiKeyWhenRequired(apiKey: ApiKey) {
-    val oldSize = apiKey.scopesEnum.size
-    apiKey.scopesEnum.removeIf {
-      getProjectPermissionScopes(
-        apiKey.project.id,
-        apiKey.userAccount.id,
-      )?.contains(it) != true
-    }
-    if (oldSize != apiKey.scopesEnum.size) {
-      apiKeyService.save(apiKey)
-    }
-  }
-
   fun checkApiKeyScopes(
     scopes: Set<Scope>,
     apiKey: ApiKeyDto,
   ) {
     checkApiKeyScopes(apiKey) { expandedScopes ->
@@ -320,15 +330,15 @@ class SecurityService(
       checkApiKeyScopes(setOf(Scope.SCREENSHOTS_UPLOAD), authenticationFacade.projectApiKey)
     }
     checkProjectPermission(projectId, Scope.SCREENSHOTS_UPLOAD)
   }
 
-  fun getProjectPermissionScopes(
+  fun getProjectPermissionScopesNoApiKey(
     projectId: Long,
     userId: Long = activeUser.id,
   ): Array<Scope>? {
-    return permissionService.getProjectPermissionScopes(projectId, userId)
+    return permissionService.getProjectPermissionScopesNoApiKey(projectId, userId)
   }
 
   fun checkKeyIdsExistAndIsFromProject(
     keyIds: List<Long>,
     projectId: Long,
diff --git a/backend/security/src/main/kotlin/io/tolgee/security/authorization/ProjectAuthorizationInterceptor.kt b/backend/security/src/main/kotlin/io/tolgee/security/authorization/ProjectAuthorizationInterceptor.kt
index e75ec09a5..a207b4bdc 100644
--- a/backend/security/src/main/kotlin/io/tolgee/security/authorization/ProjectAuthorizationInterceptor.kt
+++ b/backend/security/src/main/kotlin/io/tolgee/security/authorization/ProjectAuthorizationInterceptor.kt
@@ -68,16 +68,11 @@ class ProjectAuthorizationInterceptor(
     val requiredScopes = getRequiredScopes(request, handler)
 
     val formattedRequirements = requiredScopes?.joinToString(", ") { it.value } ?: "read-only"
     logger.debug("Checking access to proj#${project.id} by user#$userId (Requires $formattedRequirements)")
 
-    val scopes =
-      if (authenticationFacade.isProjectApiKeyAuth) {
-        authenticationFacade.projectApiKey.scopes.toTypedArray()
-      } else {
-        securityService.getProjectPermissionScopes(project.id, userId) ?: emptyArray()
-      }
+    val scopes = securityService.getCurrentPermittedScopes(project.id)
 
     if (scopes.isEmpty()) {
       if (!isAdmin) {
         logger.debug(
           "Rejecting access to proj#{} for user#{} - No view permissions",
@@ -90,27 +85,27 @@ class ProjectAuthorizationInterceptor(
       }
 
       bypassed = true
     }
 
-    requiredScopes?.forEach {
-      if (!scopes.contains(it)) {
-        if (!isAdmin) {
-          logger.debug(
-            "Rejecting access to proj#{} for user#{} - Insufficient permissions",
-            project.id,
-            userId,
-          )
-
-          throw PermissionException(
-            Message.OPERATION_NOT_PERMITTED,
-            requiredScopes.map { s -> s.value },
-          )
-        }
-
-        bypassed = true
+    val missingScopes = getMissingScopes(requiredScopes, scopes.toSet())
+
+    if (missingScopes.isNotEmpty()) {
+      if (!isAdmin) {
+        logger.debug(
+          "Rejecting access to proj#{} for user#{} - Insufficient permissions",
+          project.id,
+          userId,
+        )
+
+        throw PermissionException(
+          Message.OPERATION_NOT_PERMITTED,
+          missingScopes.map { it.value },
+        )
       }
+
+      bypassed = true
     }
 
     if (authenticationFacade.isProjectApiKeyAuth) {
       val pak = authenticationFacade.projectApiKey
       // Verify the key matches the project
@@ -120,28 +115,11 @@ class ProjectAuthorizationInterceptor(
           project.id,
           userId,
           pak.id,
         )
 
-        throw PermissionException()
-      }
-
-      // Validate scopes set on the key
-      requiredScopes?.forEach {
-        if (!pak.scopes.contains(it)) {
-          logger.debug(
-            "Rejecting access to proj#{} for user#{} via pak#{} - Insufficient permissions granted to PAK",
-            project.id,
-            userId,
-            pak.id,
-          )
-
-          throw PermissionException(
-            Message.OPERATION_NOT_PERMITTED,
-            requiredScopes.map { s -> s.value },
-          )
-        }
+        throw PermissionException(Message.PAK_CREATED_FOR_DIFFERENT_PROJECT)
       }
     }
 
     if (bypassed) {
       logger.info(
@@ -159,10 +137,19 @@ class ProjectAuthorizationInterceptor(
       ?: throw NotFoundException(Message.ORGANIZATION_NOT_FOUND)
 
     return true
   }
 
+  private fun getMissingScopes(
+    requiredScopes: Array<Scope>?,
+    permittedScopes: Collection<Scope>?,
+  ): Set<Scope> {
+    val permitted = permittedScopes?.toSet() ?: setOf()
+    val required = requiredScopes?.toSet() ?: setOf()
+    return required - permitted
+  }
+
   private fun getRequiredScopes(
     request: HttpServletRequest,
     handler: HandlerMethod,
   ): Array<Scope>? {
     val defaultPerms = AnnotationUtils.getAnnotation(handler.method, UseDefaultPermissions::class.java)
diff --git a/backend/security/src/test/kotlin/io/tolgee/security/authorization/ProjectAuthorizationInterceptorTest.kt b/backend/security/src/test/kotlin/io/tolgee/security/authorization/ProjectAuthorizationInterceptorTest.kt
index 6baf949e6..7642e88ea 100644
--- a/backend/security/src/test/kotlin/io/tolgee/security/authorization/ProjectAuthorizationInterceptorTest.kt
+++ b/backend/security/src/test/kotlin/io/tolgee/security/authorization/ProjectAuthorizationInterceptorTest.kt
@@ -96,10 +96,11 @@ class ProjectAuthorizationInterceptorTest {
     Mockito.`when`(projectDto.id).thenReturn(1337L)
     Mockito.`when`(project.id).thenReturn(1337L)
 
     Mockito.`when`(apiKey.projectId).thenReturn(1337L)
     Mockito.`when`(apiKey.scopes).thenReturn(mutableSetOf(Scope.KEYS_CREATE))
+    Mockito.`when`(securityService.getCurrentPermittedScopes(1337L)).thenReturn(mutableSetOf(Scope.KEYS_CREATE))
   }
 
   @AfterEach
   fun resetMocks() {
     Mockito.reset(
@@ -128,49 +129,49 @@ class ProjectAuthorizationInterceptorTest {
     assertThrows<Exception> { mockMvc.perform(MockMvcRequestBuilders.get("/v2/projects/1337/nonsense-perms")) }
   }
 
   @Test
   fun `it hides the organization if the user cannot see it`() {
-    Mockito.`when`(securityService.getProjectPermissionScopes(1337L, 1337L))
-      .thenReturn(null)
+    Mockito.`when`(securityService.getCurrentPermittedScopes(1337L))
+      .thenReturn(emptySet())
 
     mockMvc.perform(MockMvcRequestBuilders.get("/v2/projects/1337/default-perms")).andIsNotFound
     mockMvc.perform(MockMvcRequestBuilders.get("/v2/projects/1337/requires-admin")).andIsNotFound
 
-    Mockito.`when`(securityService.getProjectPermissionScopes(1337L, 1337L))
-      .thenReturn(arrayOf(Scope.KEYS_VIEW))
+    Mockito.`when`(securityService.getCurrentPermittedScopes(1337L))
+      .thenReturn(setOf(Scope.KEYS_VIEW))
 
     mockMvc.perform(MockMvcRequestBuilders.get("/v2/projects/1337/default-perms")).andIsOk
   }
 
   @Test
   fun `rejects access if the user does not have the required scope (single scope)`() {
-    Mockito.`when`(securityService.getProjectPermissionScopes(1337L, 1337L))
-      .thenReturn(arrayOf(Scope.KEYS_VIEW))
+    Mockito.`when`(securityService.getCurrentPermittedScopes(1337L))
+      .thenReturn(setOf(Scope.KEYS_VIEW))
 
     mockMvc.perform(MockMvcRequestBuilders.get("/v2/projects/1337/requires-single-scope")).andIsForbidden
 
-    Mockito.`when`(securityService.getProjectPermissionScopes(1337L, 1337L))
-      .thenReturn(arrayOf(Scope.KEYS_CREATE))
+    Mockito.`when`(securityService.getCurrentPermittedScopes(1337L))
+      .thenReturn(setOf(Scope.KEYS_CREATE))
 
     mockMvc.perform(MockMvcRequestBuilders.get("/v2/projects/1337/requires-single-scope")).andIsOk
   }
 
   @Test
   fun `rejects access if the user does not have the required scope (multiple scopes)`() {
-    Mockito.`when`(securityService.getProjectPermissionScopes(1337L, 1337L))
-      .thenReturn(arrayOf(Scope.KEYS_CREATE))
+    Mockito.`when`(securityService.getCurrentPermittedScopes(1337L))
+      .thenReturn(setOf(Scope.KEYS_CREATE))
 
     mockMvc.perform(MockMvcRequestBuilders.get("/v2/projects/1337/requires-multiple-scopes")).andIsForbidden
 
-    Mockito.`when`(securityService.getProjectPermissionScopes(1337L, 1337L))
-      .thenReturn(arrayOf(Scope.MEMBERS_EDIT))
+    Mockito.`when`(securityService.getCurrentPermittedScopes(1337L))
+      .thenReturn(setOf(Scope.MEMBERS_EDIT))
 
     mockMvc.perform(MockMvcRequestBuilders.get("/v2/projects/1337/requires-multiple-scopes")).andIsForbidden
 
-    Mockito.`when`(securityService.getProjectPermissionScopes(1337L, 1337L))
-      .thenReturn(arrayOf(Scope.KEYS_CREATE, Scope.MEMBERS_EDIT))
+    Mockito.`when`(securityService.getCurrentPermittedScopes(1337L))
+      .thenReturn(setOf(Scope.KEYS_CREATE, Scope.MEMBERS_EDIT))
 
     mockMvc.perform(MockMvcRequestBuilders.get("/v2/projects/1337/requires-multiple-scopes")).andIsOk
   }
 
   @Test
@@ -186,41 +187,39 @@ class ProjectAuthorizationInterceptorTest {
 
     mockMvc.perform(MockMvcRequestBuilders.get("/v2/projects/1337/requires-single-scope")).andIsForbidden
   }
 
   @Test
-  fun `it restricts scopes to the ones set to the API key`() {
-    Mockito.`when`(securityService.getProjectPermissionScopes(1337L, 1337L))
-      .thenReturn(arrayOf(Scope.KEYS_CREATE, Scope.MEMBERS_EDIT))
+  fun `it restricts scopes (multiple scopes)`() {
+    mockMvc.perform(MockMvcRequestBuilders.get("/v2/projects/1337/requires-multiple-scopes")).andIsForbidden
 
-    mockMvc.perform(MockMvcRequestBuilders.get("/v2/projects/1337/requires-multiple-scopes")).andIsOk
+    Mockito.`when`(securityService.getCurrentPermittedScopes(1337L))
+      .thenReturn(setOf(Scope.KEYS_CREATE, Scope.MEMBERS_EDIT))
 
-    Mockito.`when`(authenticationFacade.isApiAuthentication).thenReturn(true)
-    Mockito.`when`(authenticationFacade.isProjectApiKeyAuth).thenReturn(true)
-
-    mockMvc.perform(MockMvcRequestBuilders.get("/v2/projects/1337/requires-multiple-scopes")).andIsForbidden
+    mockMvc.perform(MockMvcRequestBuilders.get("/v2/projects/1337/requires-multiple-scopes")).andIsOk
   }
 
   @Test
   fun `it does not let scopes on the key work if the authenticated user does not have them`() {
     Mockito.`when`(apiKey.scopes).thenReturn(mutableSetOf(Scope.KEYS_CREATE, Scope.MEMBERS_EDIT))
-    Mockito.`when`(securityService.getProjectPermissionScopes(1337L, 1337L))
-      .thenReturn(arrayOf(Scope.KEYS_CREATE))
+    Mockito.`when`(securityService.getCurrentPermittedScopes(1337L))
+      .thenReturn(setOf(Scope.KEYS_CREATE))
 
     mockMvc.perform(MockMvcRequestBuilders.get("/v2/projects/1337/requires-multiple-scopes")).andIsForbidden
   }
 
   @Test
   fun `permissions work as intended when using implicit project id`() {
     Mockito.`when`(authenticationFacade.isApiAuthentication).thenReturn(true)
     Mockito.`when`(authenticationFacade.isProjectApiKeyAuth).thenReturn(true)
+    Mockito.`when`(securityService.getCurrentPermittedScopes(1337L)).thenReturn(setOf(Scope.KEYS_CREATE))
 
     mockMvc.perform(MockMvcRequestBuilders.get("/v2/projects/implicit-access")).andIsOk
 
     Mockito.`when`(apiKey.scopes).thenReturn(mutableSetOf(Scope.KEYS_VIEW))
-    Mockito.`when`(securityService.getProjectPermissionScopes(1337L, 1337L))
-      .thenReturn(arrayOf(Scope.KEYS_VIEW))
+    Mockito.`when`(securityService.getCurrentPermittedScopes(1337L))
+      .thenReturn(setOf(Scope.KEYS_VIEW))
 
     mockMvc.perform(MockMvcRequestBuilders.get("/v2/projects/implicit-access")).andIsForbidden
 
     Mockito.`when`(requestContextService.getTargetProject(any())).thenThrow(ProjectNotSelectedException())
 
diff --git a/backend/testing/src/main/kotlin/io/tolgee/fixtures/ProjectApiKeyAuthRequestPerformer.kt b/backend/testing/src/main/kotlin/io/tolgee/fixtures/ProjectApiKeyAuthRequestPerformer.kt
index f55129e10..bb825d9bf 100644
--- a/backend/testing/src/main/kotlin/io/tolgee/fixtures/ProjectApiKeyAuthRequestPerformer.kt
+++ b/backend/testing/src/main/kotlin/io/tolgee/fixtures/ProjectApiKeyAuthRequestPerformer.kt
@@ -23,11 +23,11 @@ class ProjectApiKeyAuthRequestPerformer(
 ) : ProjectAuthRequestPerformer(userAccountProvider, projectUrlPrefix) {
   @field:Autowired
   lateinit var apiKeyService: ApiKeyService
 
   val apiKey: ApiKeyDTO by lazy {
-    io.tolgee.dtos.response.ApiKeyDTO.fromEntity(
+    ApiKeyDTO.fromEntity(
       apiKeyService.create(userAccountProvider.invoke(), scopes = this.scopes.toSet(), project),
     )
   }
 
   override fun performProjectAuthPut(
diff --git a/e2e/cypress/e2e/projects/export/export.cy.ts b/e2e/cypress/e2e/projects/export/export.cy.ts
index 3c79b3998..ea360fe4e 100644
--- a/e2e/cypress/e2e/projects/export/export.cy.ts
+++ b/e2e/cypress/e2e/projects/export/export.cy.ts
@@ -58,11 +58,11 @@ describe('Export Basics', () => {
     const getFile = () => cy.readFile(downloadsFolder + '/' + fileName);
     getFile().its('test').its('test').should('eq', 'Test czech');
     getFile().its('test').its('array[0]').should('eq', 'Test czech');
   });
 
-  it('the support arrays switch works', () => {
+  it('the support arrays switch works', { retries: { runMode: 5 } }, () => {
     exportToggleLanguage('English');
     exportSelectFormat('Structured JSON');
 
     cy.gcy('export-support_arrays-selector').click();
     cy.waitForDom();

Vulnerable code:     val scopes =
      when {
        apiKeyAuthentication -> authenticationFacade.projectApiKey.scopes.toTypedArray()
        else -> computed.scopes
      }
      scopes = scopes,
import io.tolgee.model.Screenshot
  @UseDefaultPermissions // Security: check done internally
    val keysWithScreenshots = getScreenshots(data.map { it.keyId }.toList())

    if (keysWithScreenshots != null) {
      data.content.forEach { it.screenshots = keysWithScreenshots[it.keyId] ?: listOf() }
    }
  private fun getScreenshots(keyIds: Collection<Long>): Map<Long, List<Screenshot>>? {
    if (
      !authenticationFacade.isProjectApiKeyAuth ||
      authenticationFacade.projectApiKey.scopes.contains(Scope.SCREENSHOTS_VIEW)
    ) {
      return screenshotService.getScreenshotsForKeys(keyIds)
    }
    return null
  }

  private fun checkKeyEditScope() {
    securityService.checkProjectPermission(
      projectHolder.project.id,
      Scope.KEYS_EDIT,
    )
  }

    assertThat(permissionService.getProjectPermissionScopes(project.id, newUser)).isNotNull
    val type = permissionService.getProjectPermissionScopes(project.id, newUser)!!
      permissionService.getProjectPermissionScopes(project.id, user)
open class ProjectsControllerTest : ProjectAuthControllerTest("/v2/projects/") {
    permissionService.getProjectPermissionScopes(repo.id, user)
      return hierarchyItems.flatMap { expand(it) }.toTypedArray()
    fun expand(permittedScopes: Array<Scope>): Array<Scope> {
    val canViewTranslations =
      permissionService.getProjectPermissionScopes(projectId, userId)?.contains(Scope.TRANSLATIONS_VIEW) == true
      permissionService.getProjectPermissionScopes(project.id, userAccountId)
  fun getProjectPermissionScopes(
  ) = getProjectPermissionScopes(projectId, userAccount.id)
  fun getProjectPermissionScopes(
import io.tolgee.model.ApiKey
      getProjectPermissionScopes(projectId).isNullOrEmpty() &&
      getProjectPermissionScopes(projectId, userAccountDto.id)
  fun checkProjectPermission(
    projectId: Long,
    requiredPermission: Scope,
  ) {
    // Always check for the current user even if we're using an API key for security reasons.
    // This prevents improper preservation of permissions.
    checkProjectPermissionNoApiKey(projectId, requiredPermission, activeUser)

    val apiKey = activeApiKey ?: return
    checkProjectPermission(projectId, requiredPermission, apiKey)
  }

  fun fixInvalidApiKeyWhenRequired(apiKey: ApiKey) {
    val oldSize = apiKey.scopesEnum.size
    apiKey.scopesEnum.removeIf {
      getProjectPermissionScopes(
        apiKey.project.id,
        apiKey.userAccount.id,
      )?.contains(it) != true
    }
    if (oldSize != apiKey.scopesEnum.size) {
      apiKeyService.save(apiKey)
    }
  }

  fun getProjectPermissionScopes(
    return permissionService.getProjectPermissionScopes(projectId, userId)
    val scopes =
      if (authenticationFacade.isProjectApiKeyAuth) {
        authenticationFacade.projectApiKey.scopes.toTypedArray()
      } else {
        securityService.getProjectPermissionScopes(project.id, userId) ?: emptyArray()
      }
    requiredScopes?.forEach {
      if (!scopes.contains(it)) {
        if (!isAdmin) {
          logger.debug(
            "Rejecting access to proj#{} for user#{} - Insufficient permissions",
            project.id,
            userId,
          )

          throw PermissionException(
            Message.OPERATION_NOT_PERMITTED,
            requiredScopes.map { s -> s.value },
          )
        }

        bypassed = true
        throw PermissionException()
      }

      // Validate scopes set on the key
      requiredScopes?.forEach {
        if (!pak.scopes.contains(it)) {
          logger.debug(
            "Rejecting access to proj#{} for user#{} via pak#{} - Insufficient permissions granted to PAK",
            project.id,
            userId,
            pak.id,
          )

          throw PermissionException(
            Message.OPERATION_NOT_PERMITTED,
            requiredScopes.map { s -> s.value },
          )
        }
    Mockito.`when`(securityService.getProjectPermissionScopes(1337L, 1337L))
      .thenReturn(null)
    Mockito.`when`(securityService.getProjectPermissionScopes(1337L, 1337L))
      .thenReturn(arrayOf(Scope.KEYS_VIEW))
    Mockito.`when`(securityService.getProjectPermissionScopes(1337L, 1337L))
      .thenReturn(arrayOf(Scope.KEYS_VIEW))
    Mockito.`when`(securityService.getProjectPermissionScopes(1337L, 1337L))
      .thenReturn(arrayOf(Scope.KEYS_CREATE))
    Mockito.`when`(securityService.getProjectPermissionScopes(1337L, 1337L))
      .thenReturn(arrayOf(Scope.KEYS_CREATE))
    Mockito.`when`(securityService.getProjectPermissionScopes(1337L, 1337L))
      .thenReturn(arrayOf(Scope.MEMBERS_EDIT))
    Mockito.`when`(securityService.getProjectPermissionScopes(1337L, 1337L))
      .thenReturn(arrayOf(Scope.KEYS_CREATE, Scope.MEMBERS_EDIT))
  fun `it restricts scopes to the ones set to the API key`() {
    Mockito.`when`(securityService.getProjectPermissionScopes(1337L, 1337L))
      .thenReturn(arrayOf(Scope.KEYS_CREATE, Scope.MEMBERS_EDIT))
    mockMvc.perform(MockMvcRequestBuilders.get("/v2/projects/1337/requires-multiple-scopes")).andIsOk
    Mockito.`when`(authenticationFacade.isApiAuthentication).thenReturn(true)
    Mockito.`when`(authenticationFacade.isProjectApiKeyAuth).thenReturn(true)

    mockMvc.perform(MockMvcRequestBuilders.get("/v2/projects/1337/requires-multiple-scopes")).andIsForbidden
    Mockito.`when`(securityService.getProjectPermissionScopes(1337L, 1337L))
      .thenReturn(arrayOf(Scope.KEYS_CREATE))
    Mockito.`when`(securityService.getProjectPermissionScopes(1337L, 1337L))
      .thenReturn(arrayOf(Scope.KEYS_VIEW))
    io.tolgee.dtos.response.ApiKeyDTO.fromEntity(
  it('the support arrays switch works', () => {
Fixed code:       scopes = securityService.getCurrentPermittedScopes(projectIdNotNull).toTypedArray(),
import org.springframework.data.domain.Page
  @RequiresProjectPermissions(scopes = [Scope.KEYS_VIEW]) // Security: check done internally
    addScreenshotsToResponse(data)
  private fun addScreenshotsToResponse(data: Page<KeyWithTranslationsView>) {
    val canViewScreenshots = securityService.currentPermittedScopesContain(Scope.SCREENSHOTS_VIEW)

    if (!canViewScreenshots) {
      return
    }

    val keysWithScreenshots = screenshotService.getScreenshotsForKeys(data.map { it.keyId }.content)

    data.content.forEach { it.screenshots = keysWithScreenshots[it.keyId] ?: listOf() }
  }

    assertThat(permissionService.getProjectPermissionScopesNoApiKey(project.id, newUser)).isNotNull
    val type = permissionService.getProjectPermissionScopesNoApiKey(project.id, newUser)!!
import io.tolgee.fixtures.andIsForbidden
  @Test
  @ProjectApiKeyAuthTestMethod(scopes = [Scope.KEYS_VIEW])
  fun `returns empty translations when api key is missing the translations-view scope`() {
    testData.addKeysViewOnlyUser()
    testDataService.saveTestData(testData.root)
    userAccount = testData.user
    performProjectAuthGet("/translations?sort=id").andIsOk.andAssertThatJson {
      node("_embedded.keys[2].translations.de").isAbsent()
    }
  }

  @ProjectApiKeyAuthTestMethod(scopes = [Scope.BATCH_JOBS_VIEW])
  @Test
  fun `checks keys view scope (missing scope)`() {
    testDataService.saveTestData(testData.root)
    userAccount = testData.user
    performProjectAuthGet("/translations").andIsForbidden
  }

  @ProjectApiKeyAuthTestMethod(scopes = [Scope.TRANSLATIONS_VIEW])
  @Test
  fun `returns no screenshots when screenshot view scope is missing`() {
    testData.addKeysWithScreenshots()
    testDataService.saveTestData(testData.root)
    userAccount = testData.user
    performProjectAuthGet("/translations").andPrettyPrint.andAssertThatJson {
      node("_embedded.keys[3].screenshots").isNull()
      node("_embedded.keys[3].screenshotCount").isEqualTo(2)
    }
  }

      permissionService.getProjectPermissionScopesNoApiKey(project.id, user)
class ProjectsControllerTest : ProjectAuthControllerTest("/v2/projects/") {
    permissionService.getProjectPermissionScopesNoApiKey(repo.id, user)
  PAK_CREATED_FOR_DIFFERENT_PROJECT,
      return hierarchyItems.flatMap { expand(it) }.toSet().toTypedArray()
    fun expand(permittedScopes: Array<Scope>?): Array<Scope> {
      permittedScopes ?: return arrayOf()
    val canViewTranslations = securityService.currentPermittedScopesContain(Scope.TRANSLATIONS_VIEW)
      permissionService.getProjectPermissionScopesNoApiKey(project.id, userAccountId)
  fun getProjectPermissionScopesNoApiKey(
  ) = getProjectPermissionScopesNoApiKey(projectId, userAccount.id)
  fun getProjectPermissionScopesNoApiKey(
import io.tolgee.security.ProjectHolder
  private val projectHolder: ProjectHolder,
      getProjectPermissionScopesNoApiKey(projectId).isNullOrEmpty() &&
  fun currentPermittedScopesContain(scope: Scope): Boolean {
    return currentPermittedScopesContain(listOf(scope))
  }

  fun currentPermittedScopesContain(scopes: Collection<Scope>?): Boolean {
    scopes ?: return true
    return getCurrentPermittedScopes(projectHolder.project.id).containsAll(scopes)
  }

  /**
   * Returns current permitted scopes, expanded
   */
  fun getCurrentPermittedScopes(projectId: Long): Set<Scope> {
    val projectScopes =
      Scope.expand(
        getProjectPermissionScopesNoApiKey(projectId, authenticationFacade.authenticatedUser.id),
      ).toSet()
    val apiKey = activeApiKey ?: return projectScopes

    return Scope.expand(apiKey.scopes).toSet().intersect(projectScopes.toSet())
  }

  fun checkProjectPermission(
    projectId: Long,
    requiredPermission: Scope,
  ) {
    // Always check for the current user even if we're using an API key for security reasons.
    // This prevents improper preservation of permissions.
    checkProjectPermissionNoApiKey(projectId, requiredPermission, activeUser)

    val apiKey = activeApiKey ?: return
    checkProjectPermission(projectId, requiredPermission, apiKey)
  }

      getProjectPermissionScopesNoApiKey(projectId, userAccountDto.id)
  fun getProjectPermissionScopesNoApiKey(
    return permissionService.getProjectPermissionScopesNoApiKey(projectId, userId)
    val scopes = securityService.getCurrentPermittedScopes(project.id)
    val missingScopes = getMissingScopes(requiredScopes, scopes.toSet())

    if (missingScopes.isNotEmpty()) {
      if (!isAdmin) {
        logger.debug(
          "Rejecting access to proj#{} for user#{} - Insufficient permissions",
          project.id,
          userId,
        )

        throw PermissionException(
          Message.OPERATION_NOT_PERMITTED,
          missingScopes.map { it.value },
        )

      bypassed = true
        throw PermissionException(Message.PAK_CREATED_FOR_DIFFERENT_PROJECT)
  private fun getMissingScopes(
    requiredScopes: Array<Scope>?,
    permittedScopes: Collection<Scope>?,
  ): Set<Scope> {
    val permitted = permittedScopes?.toSet() ?: setOf()
    val required = requiredScopes?.toSet() ?: setOf()
    return required - permitted
  }

    Mockito.`when`(securityService.getCurrentPermittedScopes(1337L)).thenReturn(mutableSetOf(Scope.KEYS_CREATE))
    Mockito.`when`(securityService.getCurrentPermittedScopes(1337L))
      .thenReturn(emptySet())
    Mockito.`when`(securityService.getCurrentPermittedScopes(1337L))
      .thenReturn(setOf(Scope.KEYS_VIEW))
    Mockito.`when`(securityService.getCurrentPermittedScopes(1337L))
      .thenReturn(setOf(Scope.KEYS_VIEW))
    Mockito.`when`(securityService.getCurrentPermittedScopes(1337L))
      .thenReturn(setOf(Scope.KEYS_CREATE))
    Mockito.`when`(securityService.getCurrentPermittedScopes(1337L))
      .thenReturn(setOf(Scope.KEYS_CREATE))
    Mockito.`when`(securityService.getCurrentPermittedScopes(1337L))
      .thenReturn(setOf(Scope.MEMBERS_EDIT))
    Mockito.`when`(securityService.getCurrentPermittedScopes(1337L))
      .thenReturn(setOf(Scope.KEYS_CREATE, Scope.MEMBERS_EDIT))
  fun `it restricts scopes (multiple scopes)`() {
    mockMvc.perform(MockMvcRequestBuilders.get("/v2/projects/1337/requires-multiple-scopes")).andIsForbidden
    Mockito.`when`(securityService.getCurrentPermittedScopes(1337L))
      .thenReturn(setOf(Scope.KEYS_CREATE, Scope.MEMBERS_EDIT))
    mockMvc.perform(MockMvcRequestBuilders.get("/v2/projects/1337/requires-multiple-scopes")).andIsOk
    Mockito.`when`(securityService.getCurrentPermittedScopes(1337L))
      .thenReturn(setOf(Scope.KEYS_CREATE))
    Mockito.`when`(securityService.getCurrentPermittedScopes(1337L)).thenReturn(setOf(Scope.KEYS_CREATE))
    Mockito.`when`(securityService.getCurrentPermittedScopes(1337L))
      .thenReturn(setOf(Scope.KEYS_VIEW))
    ApiKeyDTO.fromEntity(
  it('the support arrays switch works', { retries: { runMode: 5 } }, () => {