CVE-2024-32466 (published 2024-04-18T15:15Z)

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\n


Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/tolgee/tolgee-platform
commit_details: repo_url: https://github.com/tolgee/tolgee-platform
author: Jan Cizmar
author_date: 2024-04-17 14:26:51+02:00
msg: fix: Translation endpoints permission check (#2254)
repository: repo_url: https://github.com/tolgee/tolgee-platform
repo_name: tolgee/tolgee-platform
description: Developer & translator friendly web-based localization platform
repo_language: Kotlin
owner: tolgee
file_changes: filename: ApiKeyController.kt
change_type: ModificationType.MODIFY
diff: @@ -165,11 +165,6 @@ class ApiKeyController(
       )
 
     val computed = permissionData.computedPermissions
-    val scopes =
-      when {
-        apiKeyAuthentication -> authenticationFacade.projectApiKey.scopes.toTypedArray()
-        else -> computed.scopes
-      }
 
     return ApiKeyPermissionsModel(
       projectIdNotNull,
@@ -177,7 +172,7 @@ class ApiKeyController(
       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)),
     )
   }

programming_language: Scala
filename: TranslationsController.kt
change_type: ModificationType.MODIFY
diff: @@ -28,7 +28,6 @@ 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
@@ -47,6 +46,7 @@ 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
@@ -229,7 +229,7 @@ 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)
@@ -252,16 +252,24 @@ When null, resulting file will be a flat key-value object.
       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)
@@ -317,23 +325,6 @@ When null, resulting file will be a flat key-value object.
     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) {

programming_language: Scala
filename: V2InvitationControllerTest.kt
change_type: ModificationType.MODIFY
diff: @@ -111,8 +111,8 @@ class V2InvitationControllerTest : AuthorizedControllerTest() {
     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)
   }
 

programming_language: Scala
filename: TranslationsControllerViewTest.kt
change_type: ModificationType.MODIFY
diff: @@ -4,6 +4,7 @@ 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
@@ -113,6 +114,17 @@ class TranslationsControllerViewTest : ProjectAuthControllerTest("/v2/projects/"
     }
   }
 
+  @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`() {
@@ -239,6 +251,26 @@ class TranslationsControllerViewTest : ProjectAuthControllerTest("/v2/projects/"
       }
   }
 
+  @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`() {

programming_language: Scala
filename: ProjectsControllerPermissionsTest.kt
change_type: ModificationType.MODIFY
diff: @@ -27,7 +27,7 @@ class ProjectsControllerPermissionsTest : ProjectAuthControllerTest("/v2/project
     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) }
     }
   }

programming_language: Scala
filename: ProjectsControllerTest.kt
change_type: ModificationType.MODIFY
diff: @@ -22,7 +22,7 @@ import org.springframework.boot.test.context.SpringBootTest
 
 @SpringBootTest
 @AutoConfigureMockMvc
-open class ProjectsControllerTest : ProjectAuthControllerTest("/v2/projects/") {
+class ProjectsControllerTest : ProjectAuthControllerTest("/v2/projects/") {
   @Test
   fun getAll() {
     executeInNewTransaction {
@@ -242,7 +242,7 @@ open class ProjectsControllerTest : ProjectAuthControllerTest("/v2/projects/") {
 
     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() }
   }
 

programming_language: Scala
filename: Message.kt
change_type: ModificationType.MODIFY
diff: @@ -207,6 +207,7 @@ enum class Message {
   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

programming_language: Shell
filename: Scope.kt
change_type: ModificationType.MODIFY
diff: @@ -142,7 +142,7 @@ enum class Scope(
 
     fun expand(scope: Scope): Array<Scope> {
       val hierarchyItems = getScopeHierarchyItems(scope)
-      return hierarchyItems.flatMap { expand(it) }.toTypedArray()
+      return hierarchyItems.flatMap { expand(it) }.toSet().toTypedArray()
     }
 
     /**
@@ -152,7 +152,8 @@ enum class Scope(
      * 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()
     }
 

programming_language: JavaScript
filename: LanguageService.kt
change_type: ModificationType.MODIFY
diff: @@ -223,8 +223,7 @@ class LanguageService(
     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()

programming_language: Scala
filename: ApiKeyService.kt
change_type: ModificationType.MODIFY
diff: @@ -128,7 +128,7 @@ class ApiKeyService(
     project: Project,
   ): Array<Scope> {
     val permittedScopes =
-      permissionService.getProjectPermissionScopes(project.id, userAccountId)
+      permissionService.getProjectPermissionScopesNoApiKey(project.id, userAccountId)
         ?: throw NotFoundException()
     return Scope.expand(permittedScopes)
   }

programming_language: Scala
filename: PermissionService.kt
change_type: ModificationType.MODIFY
diff: @@ -67,12 +67,12 @@ class PermissionService(
     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>? {

programming_language: Scala
filename: SecurityService.kt
change_type: ModificationType.MODIFY
diff: @@ -7,12 +7,12 @@ 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
@@ -23,6 +23,7 @@ class SecurityService(
   private val authenticationFacade: AuthenticationFacade,
   private val languageService: LanguageService,
   private val keyRepository: KeyRepository,
+  private val projectHolder: ProjectHolder,
 ) {
   @set:Autowired
   lateinit var apiKeyService: ApiKeyService
@@ -35,13 +36,47 @@ class SecurityService(
 
   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,
@@ -67,7 +102,7 @@ class SecurityService(
     }
 
     val allowedScopes =
-      getProjectPermissionScopes(projectId, userAccountDto.id)
+      getProjectPermissionScopesNoApiKey(projectId, userAccountDto.id)
         ?: throw PermissionException(Message.USER_HAS_NO_PROJECT_ACCESS)
 
     checkPermission(requiredScope, allowedScopes)
@@ -85,18 +120,6 @@ class SecurityService(
     }
   }
 
-  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>,
@@ -269,19 +292,6 @@ class SecurityService(
     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,
@@ -322,11 +332,11 @@ class SecurityService(
     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(

programming_language: Scala
filename: ProjectAuthorizationInterceptor.kt
change_type: ModificationType.MODIFY
diff: @@ -70,12 +70,7 @@ class ProjectAuthorizationInterceptor(
     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) {
@@ -92,23 +87,23 @@ 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) {
@@ -122,24 +117,7 @@ class ProjectAuthorizationInterceptor(
           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)
       }
     }
 
@@ -161,6 +139,15 @@ class ProjectAuthorizationInterceptor(
     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,

programming_language: Scala
filename: ProjectAuthorizationInterceptorTest.kt
change_type: ModificationType.MODIFY
diff: @@ -98,6 +98,7 @@ class ProjectAuthorizationInterceptorTest {
 
     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
@@ -130,45 +131,45 @@ class ProjectAuthorizationInterceptorTest {
 
   @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
   }
@@ -188,23 +189,20 @@ class ProjectAuthorizationInterceptorTest {
   }
 
   @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
   }
@@ -213,12 +211,13 @@ class ProjectAuthorizationInterceptorTest {
   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
 

programming_language: Scala
filename: ProjectApiKeyAuthRequestPerformer.kt
change_type: ModificationType.MODIFY
diff: @@ -25,7 +25,7 @@ class ProjectApiKeyAuthRequestPerformer(
   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),
     )
   }

programming_language: Scala
filename: export.cy.ts
change_type: ModificationType.MODIFY
diff: @@ -60,7 +60,7 @@ describe('Export Basics', () => {
     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');
 

programming_language: TypeScript