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 b8dbdb67bd..d663f87f7d 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 @@ -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)), ) } 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 6858a7d275..9bb4756515 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 @@ -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) { + 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): Map>? { - 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) { 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 4466bfc3d5..142a670552 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 @@ -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) } 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 7b748176e8..c605fdeb88 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 @@ -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`() { 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 0b811b5439..a76c4bef38 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 @@ -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) } } } 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 62be1d1f0e..fb0edd44ab 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 @@ -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() } } 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 f1a43d0ffc..ecf5c2011e 100644 --- a/backend/data/src/main/kotlin/io/tolgee/constants/Message.kt +++ b/backend/data/src/main/kotlin/io/tolgee/constants/Message.kt @@ -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 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 acc7ed18c2..500ce29f4a 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 @@ -142,7 +142,7 @@ enum class Scope( fun expand(scope: Scope): Array { 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): Array { + fun expand(permittedScopes: Array?): Array { + permittedScopes ?: return arrayOf() return permittedScopes.flatMap { expand(it).toList() }.toSet().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 6c6f06ee00..e5a2c10098 100644 --- a/backend/data/src/main/kotlin/io/tolgee/service/LanguageService.kt +++ b/backend/data/src/main/kotlin/io/tolgee/service/LanguageService.kt @@ -223,8 +223,7 @@ class LanguageService( projectId: Long, userId: Long, ): Set { - val canViewTranslations = - permissionService.getProjectPermissionScopes(projectId, userId)?.contains(Scope.TRANSLATIONS_VIEW) == true + val canViewTranslations = securityService.currentPermittedScopesContain(Scope.TRANSLATIONS_VIEW) if (!canViewTranslations) { return emptySet() 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 cbf14be863..93e5a4038e 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 @@ -128,7 +128,7 @@ class ApiKeyService( project: Project, ): Array { val permittedScopes = - permissionService.getProjectPermissionScopes(project.id, userAccountId) + permissionService.getProjectPermissionScopesNoApiKey(project.id, userAccountId) ?: throw NotFoundException() return Scope.expand(permittedScopes) } 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 c5f78c8e58..16b1562a63 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 @@ -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? { 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 b5e5632c4b..e48f660b7b 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 @@ -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?): Boolean { + scopes ?: return true + return getCurrentPermittedScopes(projectHolder.project.id).containsAll(scopes) + } + + /** + * Returns current permitted scopes, expanded + */ + fun getCurrentPermittedScopes(projectId: Long): Set { + 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, @@ -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, apiKey: ApiKeyDto, @@ -322,11 +332,11 @@ class SecurityService( checkProjectPermission(projectId, Scope.SCREENSHOTS_UPLOAD) } - fun getProjectPermissionScopes( + fun getProjectPermissionScopesNoApiKey( projectId: Long, userId: Long = activeUser.id, ): Array? { - return permissionService.getProjectPermissionScopes(projectId, userId) + return permissionService.getProjectPermissionScopesNoApiKey(projectId, userId) } fun checkKeyIdsExistAndIsFromProject( 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 e75ec09a58..a207b4bdcd 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 @@ -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?, + permittedScopes: Collection?, + ): Set { + val permitted = permittedScopes?.toSet() ?: setOf() + val required = requiredScopes?.toSet() ?: setOf() + return required - permitted + } + private fun getRequiredScopes( request: HttpServletRequest, handler: HandlerMethod, 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 6baf949e65..7642e88ea8 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 @@ -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 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 f55129e100..bb825d9bfd 100644 --- a/backend/testing/src/main/kotlin/io/tolgee/fixtures/ProjectApiKeyAuthRequestPerformer.kt +++ b/backend/testing/src/main/kotlin/io/tolgee/fixtures/ProjectApiKeyAuthRequestPerformer.kt @@ -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), ) } diff --git a/e2e/cypress/e2e/projects/export/export.cy.ts b/e2e/cypress/e2e/projects/export/export.cy.ts index 3c79b3998a..ea360fe4e9 100644 --- a/e2e/cypress/e2e/projects/export/export.cy.ts +++ b/e2e/cypress/e2e/projects/export/export.cy.ts @@ -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');