CVE-2023-38510: Missing Authorization (CWE-862)
value: Tolgee is an open-source localization platform. Starting in version 3.14.0 and prior to version 3.23.1, when a request is made using an API key, the backend fails to verify the permission scopes associated with the key, effectively bypassing permission checks entirely for some endpoints. It's important to note that this vulnerability only affects projects that have inadvertently exposed their API keys on the internet. Projects that have kept their API keys secure are not impacted. This issue is fixed in version 3.23.1.
CWE description: The product does not perform an authorization check when an actor attempts to access a resource or perform an action.
CVSS v3 base score: 8.1
Published: 2023-07-27T19:15Z
Repository: https://github.com/tolgee/tolgee-platform
Language: Kotlin
Files changed: backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/V2ImportController.kt, backend/app/src/main/kotlin/io/tolgee/websocket/WebSocketConfig.kt, backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ImportController/V2ImportControllerApplicationTest.kt, backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2KeyController/KeyControllerCreationTest.kt, backend/app/src/test/kotlin/io/tolgee/service/TelemetryServiceTest.kt, backend/app/src/test/resources/application.yaml, backend/data/src/main/kotlin/io/tolgee/repository/dataImport/ImportLanguageRepository.kt, backend/data/src/main/kotlin/io/tolgee/service/dataImport/StoredDataImporter.kt, backend/data/src/main/kotlin/io/tolgee/service/security/SecurityService.kt, ee/backend/tests/src/test/resources/application.yaml
Commit date: 2023-07-21 16:30:34 +0200
Commit message: fix: Disable importing with any project permission (#1818)


Diff: diff --git a/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/V2ImportController.kt b/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/V2ImportController.kt
index 4d4ec6f1..be55cca3 100644
--- a/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/V2ImportController.kt
+++ b/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/V2ImportController.kt
@@ -23,10 +23,11 @@ import io.tolgee.hateoas.dataImport.ImportTranslationModel
 import io.tolgee.hateoas.dataImport.ImportTranslationModelAssembler
 import io.tolgee.model.Language
 import io.tolgee.model.dataImport.ImportFile
 import io.tolgee.model.dataImport.ImportLanguage
 import io.tolgee.model.dataImport.ImportTranslation
+import io.tolgee.model.enums.Scope
 import io.tolgee.model.views.ImportFileIssueView
 import io.tolgee.model.views.ImportLanguageView
 import io.tolgee.model.views.ImportTranslationView
 import io.tolgee.security.AuthenticationFacade
 import io.tolgee.security.apiKeyAuth.AccessWithApiKey
@@ -34,10 +35,11 @@ import io.tolgee.security.project_auth.AccessWithAnyProjectPermission
 import io.tolgee.security.project_auth.ProjectHolder
 import io.tolgee.service.LanguageService
 import io.tolgee.service.dataImport.ForceMode
 import io.tolgee.service.dataImport.ImportService
 import io.tolgee.service.key.NamespaceService
+import io.tolgee.service.security.SecurityService
 import org.springdoc.api.annotations.ParameterObject
 import org.springframework.data.domain.PageRequest
 import org.springframework.data.domain.Pageable
 import org.springframework.data.web.PagedResourcesAssembler
 import org.springframework.data.web.SortDefault
@@ -62,11 +64,14 @@ import javax.servlet.http.HttpServletRequest
 
 @Suppress("MVCPathVariableInspection")
 @RestController
 @CrossOrigin(origins = ["*"])
 @RequestMapping(value = ["/v2/projects/{projectId:\\d+}/import", "/v2/projects/import"])
-@Tag(name = "Import")
+@Tag(
+  name = "Import",
+  description = "These endpoints handle multi-step data import"
+)
 class V2ImportController(
   private val importService: ImportService,
   private val authenticationFacade: AuthenticationFacade,
   private val importLanguageModelAssembler: ImportLanguageModelAssembler,
   private val importTranslationModelAssembler: ImportTranslationModelAssembler,
@@ -79,19 +84,21 @@ class V2ImportController(
   @Suppress("SpringJavaInjectionPointsAutowiringInspection")
   private val pagedImportFileIssueResourcesAssembler: PagedResourcesAssembler<ImportFileIssueView>,
   private val projectHolder: ProjectHolder,
   private val languageService: LanguageService,
   private val namespaceService: NamespaceService,
+  private val securityService: SecurityService
 ) {
   @PostMapping("", consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
   @AccessWithAnyProjectPermission()
   @AccessWithApiKey()
   @Operation(description = "Prepares provided files to import.", summary = "Add files")
   fun addFiles(
     @RequestPart("files") files: Array<MultipartFile>,
     @ParameterObject params: ImportAddFilesParams
   ): ImportAddFilesResultModel {
+    checkBaseImportPermissions()
     val fileDtos = files.map { ImportFileDto(it.originalFilename ?: "", it.inputStream) }
     val errors = importService.addFiles(
       files = fileDtos,
       project = projectHolder.projectEntity,
       userAccount = authenticationFacade.userAccountEntity,
@@ -120,20 +127,26 @@ class V2ImportController(
     @Parameter(description = "Whether override or keep all translations with unresolved conflicts")
     @RequestParam(defaultValue = "NO_FORCE")
     forceMode: ForceMode,
   ) {
     val projectId = projectHolder.project.id
+    checkBaseImportPermissions()
     this.importService.import(projectId, authenticationFacade.userAccount.id, forceMode)
   }
 
+  private fun checkBaseImportPermissions() {
+    securityService.checkProjectPermission(projectHolder.project.id, Scope.TRANSLATIONS_VIEW)
+  }
+
   @GetMapping("/result")
   @AccessWithAnyProjectPermission()
   @AccessWithApiKey()
   @Operation(description = "Returns the result of preparation.", summary = "Get result")
   fun getImportResult(
     @ParameterObject pageable: Pageable
   ): PagedModel<ImportLanguageModel> {
+    checkBaseImportPermissions()
     val projectId = projectHolder.project.id
     val userId = authenticationFacade.userAccount.id
     val languages = importService.getResult(projectId, userId, pageable)
     return pagedLanguagesResourcesAssembler.toModel(languages, importLanguageModelAssembler)
   }
@@ -143,10 +156,11 @@ class V2ImportController(
   @AccessWithApiKey()
   @Operation(description = "Returns language prepared to import.", summary = "Get import language")
   fun getImportLanguage(
     @PathVariable("languageId") languageId: Long,
   ): ImportLanguageModel {
+    checkBaseImportPermissions()
     checkImportLanguageInProject(languageId)
     val language = importService.findLanguageView(languageId) ?: throw NotFoundException()
     return importLanguageModelAssembler.toModel(language)
   }
 
@@ -169,28 +183,31 @@ class V2ImportController(
     @RequestParam("onlyUnresolved", defaultValue = "false") onlyUnresolved: Boolean = false,
     @Parameter(description = "String to search in translation text or key")
     @RequestParam("search") search: String? = null,
     @ParameterObject @SortDefault("keyName") pageable: Pageable
   ): PagedModel<ImportTranslationModel> {
+    checkBaseImportPermissions()
     checkImportLanguageInProject(languageId)
     val translations = importService.getTranslationsView(languageId, pageable, onlyConflicts, onlyUnresolved, search)
     return pagedTranslationsResourcesAssembler.toModel(translations, importTranslationModelAssembler)
   }
 
   @DeleteMapping("")
   @AccessWithAnyProjectPermission()
   @AccessWithApiKey()
   @Operation(description = "Deletes prepared import data.", summary = "Delete")
   fun cancelImport() {
+    checkBaseImportPermissions()
     this.importService.deleteImport(projectHolder.project.id, authenticationFacade.userAccount.id)
   }
 
   @DeleteMapping("/result/languages/{languageId}")
   @AccessWithAnyProjectPermission()
   @AccessWithApiKey()
   @Operation(description = "Deletes language prepared to import.", summary = "Delete language")
   fun deleteLanguage(@PathVariable("languageId") languageId: Long) {
+    checkBaseImportPermissions()
     val language = checkImportLanguageInProject(languageId)
     this.importService.deleteLanguage(language)
   }
 
   @PutMapping("/result/languages/{languageId}/translations/{translationId}/resolve/set-override")
@@ -202,10 +219,11 @@ class V2ImportController(
   )
   fun resolveTranslationSetOverride(
     @PathVariable("languageId") languageId: Long,
     @PathVariable("translationId") translationId: Long
   ) {
+    checkBaseImportPermissions()
     resolveTranslation(languageId, translationId, true)
   }
 
   @PutMapping("/result/languages/{languageId}/translations/{translationId}/resolve/set-keep-existing")
   @AccessWithAnyProjectPermission()
@@ -216,10 +234,11 @@ class V2ImportController(
   )
   fun resolveTranslationSetKeepExisting(
     @PathVariable("languageId") languageId: Long,
     @PathVariable("translationId") translationId: Long
   ) {
+    checkBaseImportPermissions()
     resolveTranslation(languageId, translationId, false)
   }
 
   @PutMapping("/result/languages/{languageId}/resolve-all/set-override")
   @AccessWithAnyProjectPermission()
@@ -229,10 +248,11 @@ class V2ImportController(
     summary = "Resolve all translation conflicts (override)"
   )
   fun resolveTranslationSetOverride(
     @PathVariable("languageId") languageId: Long
   ) {
+    checkBaseImportPermissions()
     resolveAllOfLanguage(languageId, true)
   }
 
   @PutMapping("/result/languages/{languageId}/resolve-all/set-keep-existing")
   @AccessWithAnyProjectPermission()
@@ -242,10 +262,11 @@ class V2ImportController(
     summary = "Resolve all translation conflicts (keep existing)"
   )
   fun resolveTranslationSetKeepExisting(
     @PathVariable("languageId") languageId: Long,
   ) {
+    checkBaseImportPermissions()
     resolveAllOfLanguage(languageId, false)
   }
 
   @PutMapping("/result/files/{fileId}/select-namespace")
   @AccessWithAnyProjectPermission()
@@ -257,10 +278,11 @@ class V2ImportController(
   fun selectNamespace(
     @PathVariable fileId: Long,
     @RequestBody req: SetFileNamespaceRequest,
     request: HttpServletRequest
   ) {
+    checkBaseImportPermissions()
     val file = checkFileFromProject(fileId)
     this.importService.selectNamespace(file, req.namespace)
   }
 
   @PutMapping("/result/languages/{importLanguageId}/select-existing/{existingLanguageId}")
@@ -273,10 +295,11 @@ class V2ImportController(
   )
   fun selectExistingLanguage(
     @PathVariable("importLanguageId") importLanguageId: Long,
     @PathVariable("existingLanguageId") existingLanguageId: Long,
   ) {
+    checkBaseImportPermissions()
     val existingLanguage = checkLanguageFromProject(existingLanguageId)
     val importLanguage = checkImportLanguageInProject(importLanguageId)
     this.importService.selectExistingLanguage(importLanguage, existingLanguage)
   }
 
@@ -288,10 +311,11 @@ class V2ImportController(
     summary = "Reset existing language pairing"
   )
   fun resetExistingLanguage(
     @PathVariable("importLanguageId") importLanguageId: Long,
   ) {
+    checkBaseImportPermissions()
     val importLanguage = checkImportLanguageInProject(importLanguageId)
     this.importService.selectExistingLanguage(importLanguage, null)
   }
 
   @GetMapping("/result/files/{importFileId}/issues")
@@ -316,10 +340,11 @@ class V2ImportController(
   @Operation(
     description = "Returns all existing and imported namespaces",
     summary = "Get namespaces"
   )
   fun getAllNamespaces(): CollectionModel<ImportNamespaceModel> {
+    checkBaseImportPermissions()
     val import = importService.get(
       projectId = projectHolder.project.id,
       authorId = authenticationFacade.userAccount.id
     )
     val importNamespaces = importService.getAllNamespaces(import.id)
diff --git a/backend/app/src/main/kotlin/io/tolgee/websocket/WebSocketConfig.kt b/backend/app/src/main/kotlin/io/tolgee/websocket/WebSocketConfig.kt
index 083a01c3..08458428 100644
--- a/backend/app/src/main/kotlin/io/tolgee/websocket/WebSocketConfig.kt
+++ b/backend/app/src/main/kotlin/io/tolgee/websocket/WebSocketConfig.kt
@@ -50,11 +50,11 @@ class WebSocketConfig(
           }
 
           if (projectId != null) {
             try {
               val user = (accessor.user as? UsernamePasswordAuthenticationToken)?.principal as UserAccountDto
-              securityService.checkProjectPermission(projectId = projectId, Scope.TRANSLATIONS_VIEW, user)
+              securityService.checkProjectPermissionNoApiKey(projectId = projectId, Scope.TRANSLATIONS_VIEW, user)
             } catch (e: Exception) {
               throw MessagingException("Forbidden")
             }
           }
         }
diff --git a/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ImportController/V2ImportControllerApplicationTest.kt b/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ImportController/V2ImportControllerApplicationTest.kt
index 01277a32..c6293b29 100644
--- a/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ImportController/V2ImportControllerApplicationTest.kt
+++ b/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ImportController/V2ImportControllerApplicationTest.kt
@@ -1,20 +1,22 @@
 package io.tolgee.api.v2.controllers.v2ImportController
 
+import io.tolgee.ProjectAuthControllerTest
 import io.tolgee.constants.MtServiceType
 import io.tolgee.development.testDataBuilder.data.dataImport.ImportTestData
 import io.tolgee.fixtures.andAssertThatJson
 import io.tolgee.fixtures.andIsForbidden
 import io.tolgee.fixtures.andIsOk
 import io.tolgee.fixtures.isValidId
 import io.tolgee.fixtures.node
-import io.tolgee.testing.AuthorizedControllerTest
+import io.tolgee.model.enums.Scope
+import io.tolgee.testing.annotations.ProjectApiKeyAuthTestMethod
 import io.tolgee.testing.assert
 import io.tolgee.testing.assertions.Assertions.assertThat
 import org.junit.jupiter.api.Test
 
-class V2ImportControllerApplicationTest : AuthorizedControllerTest() {
+class V2ImportControllerApplicationTest : ProjectAuthControllerTest("/v2/projects/") {
   @Test
   fun `it applies the import`() {
     val testData = ImportTestData()
     testData.setAllResolved()
     testData.setAllOverride()
@@ -81,11 +83,11 @@ class V2ImportControllerApplicationTest : AuthorizedControllerTest() {
     loginAsUser(user.username)
 
     val path = "/v2/projects/$projectId/import/apply?forceMode=OVERRIDE"
     performAuthPut(path, null).andIsForbidden.andAssertThatJson {
       node("params") {
-        node("[0]").isEqualTo("""["keys.edit"]""")
+        node("[0]").isEqualTo(""""keys.create"""")
       }
     }
   }
 
   @Test
@@ -126,10 +128,26 @@ class V2ImportControllerApplicationTest : AuthorizedControllerTest() {
 
     val path = "/v2/projects/$projectId/import/apply?forceMode=OVERRIDE"
     performAuthPut(path, null).andIsForbidden
   }
 
+  @Test
+  @ProjectApiKeyAuthTestMethod(scopes = [Scope.TRANSLATIONS_VIEW])
+  fun `it checks permissions with API key (view only)`() {
+    val testData = ImportTestData()
+    testData.importBuilder.data.importFiles[0].data.importKeys.removeIf { it.self == testData.newLongKey }
+    val resolveFrench = testData.addFrenchTranslations()
+    resolveFrench()
+
+    testDataService.saveTestData(testData.root)
+    projectSupplier = { testData.project }
+    userAccount = testData.userAccount
+
+    val path = "import/apply?forceMode=OVERRIDE"
+    performProjectAuthPut(path, null).andIsForbidden
+  }
+
   @Test
   fun `it sets outdated on update`() {
     val testData = ImportTestData()
     testDataService.saveTestData(testData.root)
     val user = testData.root.data.userAccounts[0].self
diff --git a/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2KeyController/KeyControllerCreationTest.kt b/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2KeyController/KeyControllerCreationTest.kt
index 40dfbebd..6d4216bc 100644
--- a/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2KeyController/KeyControllerCreationTest.kt
+++ b/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2KeyController/KeyControllerCreationTest.kt
@@ -6,10 +6,11 @@ import io.tolgee.dtos.request.KeyInScreenshotPositionDto
 import io.tolgee.dtos.request.key.CreateKeyDto
 import io.tolgee.dtos.request.key.KeyScreenshotDto
 import io.tolgee.exceptions.FileStoreException
 import io.tolgee.fixtures.andAssertThatJson
 import io.tolgee.fixtures.andIsCreated
+import io.tolgee.fixtures.andIsForbidden
 import io.tolgee.fixtures.andPrettyPrint
 import io.tolgee.fixtures.isValidId
 import io.tolgee.fixtures.node
 import io.tolgee.model.enums.Scope
 import io.tolgee.service.ImageUploadService
@@ -61,20 +62,27 @@ class KeyControllerCreationTest : ProjectAuthControllerTest("/v2/projects/") {
         node("id").isValidId
         node("name").isEqualTo(KeyControllerTest.MAX_OK_NAME)
       }
   }
 
-  @ProjectApiKeyAuthTestMethod(scopes = [Scope.KEYS_CREATE])
+  @ProjectApiKeyAuthTestMethod(scopes = [Scope.KEYS_CREATE, Scope.TRANSLATIONS_EDIT])
   @Test
   fun `creates key with keys create scope`() {
     performProjectAuthPost("keys", CreateKeyDto(name = "super_key", translations = mapOf("en" to "", "de" to "")))
       .andIsCreated.andPrettyPrint.andAssertThatJson {
         node("id").isValidId
         node("name").isEqualTo("super_key")
       }
   }
 
+  @ProjectApiKeyAuthTestMethod(scopes = [Scope.KEYS_CREATE])
+  @Test
+  fun `create key with translations require translate permissions`() {
+    performProjectAuthPost("keys", CreateKeyDto(name = "super_key", translations = mapOf("en" to "", "de" to "")))
+      .andIsForbidden
+  }
+
   @ProjectJWTAuthTestMethod
   @Test
   fun `creates key with translations and tags and screenshots`() {
     val keyName = "super_key"
 
diff --git a/backend/app/src/test/kotlin/io/tolgee/service/TelemetryServiceTest.kt b/backend/app/src/test/kotlin/io/tolgee/service/TelemetryServiceTest.kt
index 94c614b2..e88d755f 100644
--- a/backend/app/src/test/kotlin/io/tolgee/service/TelemetryServiceTest.kt
+++ b/backend/app/src/test/kotlin/io/tolgee/service/TelemetryServiceTest.kt
@@ -23,11 +23,11 @@ import org.springframework.http.HttpMethod
 import org.springframework.web.client.RestTemplate
 
 @SpringBootTest(
   properties = [
     "tolgee.telemetry.report-period-ms=200",
-    "tolgee.telemetry.enabled=true"
+    "tolgee.telemetry.enabled=false"
   ]
 )
 class TelemetryServiceTest : AbstractSpringTest() {
 
   @MockBean
@@ -37,11 +37,11 @@ class TelemetryServiceTest : AbstractSpringTest() {
   @Autowired
   lateinit var telemetryProperties: TelemetryProperties
 
   @AfterEach
   fun clean() {
-    telemetryProperties.enabled = true
+    telemetryProperties.enabled = false
   }
 
   @Test
   fun `doesn't report when disabled`() {
     telemetryProperties.enabled = false
@@ -59,10 +59,11 @@ class TelemetryServiceTest : AbstractSpringTest() {
     }
   }
 
   @Test
   fun `reports when enabled`() {
+    telemetryProperties.enabled = true
     val testData = BaseTestData().apply {
       this.root.addProject { name = "bbbb" }.build {
         val en = addEnglish()
         addKey("a") {
           addTranslation {
diff --git a/backend/app/src/test/resources/application.yaml b/backend/app/src/test/resources/application.yaml
index 2e5e8e09..356d012a 100644
--- a/backend/app/src/test/resources/application.yaml
+++ b/backend/app/src/test/resources/application.yaml
@@ -64,10 +64,11 @@ tolgee:
       secretKey: aaa
   billing:
     enabled: false
   telemetry:
     enabled: false
+    server: http://localhost:8080
 logging:
   level:
     io.tolgee.billing.api.v2.OrganizationInvoicesController: DEBUG
 #    org.springframework.orm.jpa: DEBUG
 #    org.springframework.transaction: DEBUG
diff --git a/backend/data/src/main/kotlin/io/tolgee/repository/dataImport/ImportLanguageRepository.kt b/backend/data/src/main/kotlin/io/tolgee/repository/dataImport/ImportLanguageRepository.kt
index 5452f7f2..a70b103a 100644
--- a/backend/data/src/main/kotlin/io/tolgee/repository/dataImport/ImportLanguageRepository.kt
+++ b/backend/data/src/main/kotlin/io/tolgee/repository/dataImport/ImportLanguageRepository.kt
@@ -66,6 +66,17 @@ interface ImportLanguageRepository : JpaRepository<ImportLanguage, Long> {
             where il.id = :languageId
             $VIEW_GROUP_BY
             """
   )
   fun findViewById(languageId: Long): Optional<ImportLanguageView>
+
+  @Query(
+    """
+      select distinct il.existingLanguage.id 
+        from ImportLanguage il 
+        join il.file if 
+        where if.import.id = :importId 
+          and il.existingLanguage.id is not null
+    """
+  )
+  fun findAssignedExistingLanguageIds(importId: Long): List<Long>
 }
diff --git a/backend/data/src/main/kotlin/io/tolgee/service/dataImport/StoredDataImporter.kt b/backend/data/src/main/kotlin/io/tolgee/service/dataImport/StoredDataImporter.kt
index 5ef13138..b6d1e1df 100644
--- a/backend/data/src/main/kotlin/io/tolgee/service/dataImport/StoredDataImporter.kt
+++ b/backend/data/src/main/kotlin/io/tolgee/service/dataImport/StoredDataImporter.kt
@@ -122,11 +122,11 @@ class StoredDataImporter(
   }
 
   private fun checkKeyPermissions() {
     val isCreatingKey = keysToSave.values.any { it.id == 0L }
     if (isCreatingKey) {
-      securityService.checkProjectPermission(import.project.id, Scope.KEYS_EDIT)
+      securityService.checkProjectPermission(import.project.id, Scope.KEYS_CREATE)
     }
   }
 
   private fun handleKeyMetas() {
     this.importDataManager.storedKeys.entries.forEach { (fileNamePair, importKey) ->
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 88e687f4..4e650206 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
@@ -38,60 +38,51 @@ class SecurityService @Autowired constructor(
       !isCurrentUserServerAdmin()
     )
       throw PermissionException()
   }
 
-  fun checkProjectPermission(projectId: Long, requiredScopes: Scope, userAccountDto: UserAccountDto) {
-    checkProjectPermissionOr(projectId, listOf(requiredScopes), userAccountDto)
-  }
-
   fun checkProjectPermission(projectId: Long, requiredScopes: Scope, apiKey: ApiKey) {
-    checkProjectPermissionOr(listOf(requiredScopes), apiKey)
+    checkProjectPermission(listOf(requiredScopes), apiKey)
   }
 
-  private fun checkProjectPermissionOr(requiredScopes: List<Scope>, apiKey: ApiKey) {
-    this.checkApiKeyScopesOr(requiredScopes, apiKey)
+  private fun checkProjectPermission(requiredScopes: List<Scope>, apiKey: ApiKey) {
+    this.checkApiKeyScopes(requiredScopes, apiKey)
   }
 
-  fun checkProjectPermissionOr(
+  fun checkProjectPermissionNoApiKey(
     projectId: Long,
-    requiredScopes: Collection<Scope>,
+    requiredScope: Scope,
     userAccountDto: UserAccountDto
   ) {
     if (isUserAdmin(userAccountDto)) {
       return
     }
 
     val allowedScopes = getProjectPermissionScopes(projectId, userAccountDto.id)
       ?: throw PermissionException(Message.USER_HAS_NO_PROJECT_ACCESS)
 
-    checkProjectPermissionOr(projectId, requiredScopes, allowedScopes)
+    checkPermission(requiredScope, allowedScopes)
   }
 
-  fun checkProjectPermissionOr(
-    projectId: Long,
-    requiredScopes: Collection<Scope>,
+  private fun checkPermission(
+    requiredScope: Scope,
     allowedScopes: Array<Scope>
   ) {
-    if (!allowedScopes.any { requiredScopes.contains(it) }) {
+    if (!allowedScopes.contains(requiredScope)) {
       @Suppress("UNCHECKED_CAST")
       throw PermissionException(
         Message.OPERATION_NOT_PERMITTED,
-        listOf(requiredScopes.map { it.value }) as List<Serializable>
+        listOf(requiredScope.value) as List<Serializable>
       )
     }
   }
 
   fun checkProjectPermission(projectId: Long, requiredPermission: Scope) {
-    val apiKey = activeApiKey ?: return checkProjectPermission(projectId, requiredPermission, activeUser)
+    val apiKey = activeApiKey ?: return checkProjectPermissionNoApiKey(projectId, requiredPermission, activeUser)
     return checkProjectPermission(projectId, requiredPermission, apiKey)
   }
 
-  fun checkProjectPermissionOr(projectId: Long, requiredPermissions: Collection<Scope>) {
-    checkProjectPermissionOr(projectId, requiredPermissions, activeUser)
-  }
-
   fun checkLanguageViewPermissionByTag(projectId: Long, languageTags: Collection<String>) {
     checkProjectPermission(projectId, Scope.TRANSLATIONS_VIEW)
     checkLanguagePermissionByTag(
       projectId,
       languageTags
@@ -224,13 +215,14 @@ class SecurityService @Autowired constructor(
         throw PermissionException()
       }
     }
   }
 
-  fun checkApiKeyScopesOr(scopes: Collection<Scope>, apiKey: ApiKey) {
+  fun checkApiKeyScopes(scopes: Collection<Scope>, apiKey: ApiKey) {
     checkApiKeyScopes(apiKey) { expandedScopes ->
-      if (!expandedScopes.any { it in apiKey.scopesEnum }) {
+      val hasRequiredPermission = scopes.all { expandedScopes.contains(it) }
+      if (!hasRequiredPermission) {
         throw PermissionException()
       }
     }
   }
 
diff --git a/ee/backend/tests/src/test/resources/application.yaml b/ee/backend/tests/src/test/resources/application.yaml
index 97960b90..9d275955 100644
--- a/ee/backend/tests/src/test/resources/application.yaml
+++ b/ee/backend/tests/src/test/resources/application.yaml
@@ -64,10 +64,11 @@ tolgee:
       secretKey: aaa
   billing:
     enabled: false
   telemetry:
     enabled: false
+    server: http://localhost:8080
 
 management:
   endpoint:
     metrics:
       enabled: false

Vulnerable code: @Tag(name = "Import")
              securityService.checkProjectPermission(projectId = projectId, Scope.TRANSLATIONS_VIEW, user)
import io.tolgee.testing.AuthorizedControllerTest
class V2ImportControllerApplicationTest : AuthorizedControllerTest() {
        node("[0]").isEqualTo("""["keys.edit"]""")
  @ProjectApiKeyAuthTestMethod(scopes = [Scope.KEYS_CREATE])
    "tolgee.telemetry.enabled=true"
    telemetryProperties.enabled = true
      securityService.checkProjectPermission(import.project.id, Scope.KEYS_EDIT)
  fun checkProjectPermission(projectId: Long, requiredScopes: Scope, userAccountDto: UserAccountDto) {
    checkProjectPermissionOr(projectId, listOf(requiredScopes), userAccountDto)
  }

    checkProjectPermissionOr(listOf(requiredScopes), apiKey)
  private fun checkProjectPermissionOr(requiredScopes: List<Scope>, apiKey: ApiKey) {
    this.checkApiKeyScopesOr(requiredScopes, apiKey)
  fun checkProjectPermissionOr(
    requiredScopes: Collection<Scope>,
    checkProjectPermissionOr(projectId, requiredScopes, allowedScopes)
  fun checkProjectPermissionOr(
    projectId: Long,
    requiredScopes: Collection<Scope>,
    if (!allowedScopes.any { requiredScopes.contains(it) }) {
        listOf(requiredScopes.map { it.value }) as List<Serializable>
    val apiKey = activeApiKey ?: return checkProjectPermission(projectId, requiredPermission, activeUser)
  fun checkProjectPermissionOr(projectId: Long, requiredPermissions: Collection<Scope>) {
    checkProjectPermissionOr(projectId, requiredPermissions, activeUser)
  }

  fun checkApiKeyScopesOr(scopes: Collection<Scope>, apiKey: ApiKey) {
      if (!expandedScopes.any { it in apiKey.scopesEnum }) {
Fixed code: import io.tolgee.model.enums.Scope
import io.tolgee.service.security.SecurityService
@Tag(
  name = "Import",
  description = "These endpoints handle multi-step data import"
)
  private val securityService: SecurityService
    checkBaseImportPermissions()
    checkBaseImportPermissions()
  private fun checkBaseImportPermissions() {
    securityService.checkProjectPermission(projectHolder.project.id, Scope.TRANSLATIONS_VIEW)
  }

    checkBaseImportPermissions()
    checkBaseImportPermissions()
    checkBaseImportPermissions()
    checkBaseImportPermissions()
    checkBaseImportPermissions()
    checkBaseImportPermissions()
    checkBaseImportPermissions()
    checkBaseImportPermissions()
    checkBaseImportPermissions()
    checkBaseImportPermissions()
    checkBaseImportPermissions()
    checkBaseImportPermissions()
    checkBaseImportPermissions()
              securityService.checkProjectPermissionNoApiKey(projectId = projectId, Scope.TRANSLATIONS_VIEW, user)
import io.tolgee.ProjectAuthControllerTest
import io.tolgee.model.enums.Scope
import io.tolgee.testing.annotations.ProjectApiKeyAuthTestMethod
class V2ImportControllerApplicationTest : ProjectAuthControllerTest("/v2/projects/") {
        node("[0]").isEqualTo(""""keys.create"""")
  @Test
  @ProjectApiKeyAuthTestMethod(scopes = [Scope.TRANSLATIONS_VIEW])
  fun `it checks permissions with API key (view only)`() {
    val testData = ImportTestData()
    testData.importBuilder.data.importFiles[0].data.importKeys.removeIf { it.self == testData.newLongKey }
    val resolveFrench = testData.addFrenchTranslations()
    resolveFrench()

    testDataService.saveTestData(testData.root)
    projectSupplier = { testData.project }
    userAccount = testData.userAccount

    val path = "import/apply?forceMode=OVERRIDE"
    performProjectAuthPut(path, null).andIsForbidden
  }

import io.tolgee.fixtures.andIsForbidden
  @ProjectApiKeyAuthTestMethod(scopes = [Scope.KEYS_CREATE, Scope.TRANSLATIONS_EDIT])
  @ProjectApiKeyAuthTestMethod(scopes = [Scope.KEYS_CREATE])
  @Test
  fun `create key with translations require translate permissions`() {
    performProjectAuthPost("keys", CreateKeyDto(name = "super_key", translations = mapOf("en" to "", "de" to "")))
      .andIsForbidden
  }

    "tolgee.telemetry.enabled=false"
    telemetryProperties.enabled = false
    telemetryProperties.enabled = true
    server: http://localhost:8080

  @Query(
    """
      select distinct il.existingLanguage.id 
        from ImportLanguage il 
        join il.file if 
        where if.import.id = :importId 
          and il.existingLanguage.id is not null
    """
  )
  fun findAssignedExistingLanguageIds(importId: Long): List<Long>
      securityService.checkProjectPermission(import.project.id, Scope.KEYS_CREATE)
    checkProjectPermission(listOf(requiredScopes), apiKey)
  private fun checkProjectPermission(requiredScopes: List<Scope>, apiKey: ApiKey) {
    this.checkApiKeyScopes(requiredScopes, apiKey)
  fun checkProjectPermissionNoApiKey(
    requiredScope: Scope,
    checkPermission(requiredScope, allowedScopes)
  private fun checkPermission(
    requiredScope: Scope,
    if (!allowedScopes.contains(requiredScope)) {
        listOf(requiredScope.value) as List<Serializable>
    val apiKey = activeApiKey ?: return checkProjectPermissionNoApiKey(projectId, requiredPermission, activeUser)
  fun checkApiKeyScopes(scopes: Collection<Scope>, apiKey: ApiKey) {
      val hasRequiredPermission = scopes.all { expandedScopes.contains(it) }
      if (!hasRequiredPermission) {
    server: http://localhost:8080