Skip to content

Commit c66ae01

Browse files
committed
fix: resolve implicit project for single-project OAuth tokens
An OAuth token narrowed to a single project (tg.prj) carries that project the same way a PAK does, so implicit-project endpoints (no {projectId} in the path, used by the in-context SDK) can now resolve it instead of failing with project_not_selected. All-projects and multi-project tokens still throw, since the client must target a project explicitly.
1 parent b33eddb commit c66ae01

2 files changed

Lines changed: 43 additions & 3 deletions

File tree

backend/data/src/main/kotlin/io/tolgee/security/RequestContextService.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,17 @@ class RequestContextService(
6767

6868
private fun getTargetProjectImplicit(): ProjectDto? {
6969
// This method is the source of complexity for the global handling, but is itself quite simple. Oh, the irony!
70-
if (!authenticationFacade.isProjectApiKeyAuth) {
71-
throw ProjectNotSelectedException()
70+
if (authenticationFacade.isProjectApiKeyAuth) {
71+
return projectService.findDto(authenticationFacade.projectApiKey.projectId)
7272
}
7373

74-
return projectService.findDto(authenticationFacade.projectApiKey.projectId)
74+
// An OAuth token narrowed to a single project carries that project the same way a PAK does, so implicit endpoints
75+
// (no {projectId} in the path) can resolve it. All-projects or multi-project tokens can't be resolved implicitly.
76+
authenticationFacade.oauthTokenCredentials?.projectIds?.singleOrNull()?.let {
77+
return projectService.findDto(it)
78+
}
79+
80+
throw ProjectNotSelectedException()
7581
}
7682

7783
/**

backend/data/src/test/kotlin/io/tolgee/security/RequestContextServiceTest.kt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import io.tolgee.dtos.cacheable.OrganizationDto
2121
import io.tolgee.dtos.cacheable.ProjectDto
2222
import io.tolgee.exceptions.InvalidPathException
2323
import io.tolgee.security.authentication.AuthenticationFacade
24+
import io.tolgee.security.oauth2.OAuth2TokenCredentials
2425
import io.tolgee.service.organization.OrganizationService
2526
import io.tolgee.service.project.ProjectService
2627
import org.assertj.core.api.Assertions.assertThat
@@ -83,6 +84,12 @@ class RequestContextServiceTest {
8384
Mockito.`when`(authenticationFacade.projectApiKey).thenReturn(apiKey)
8485
}
8586

87+
private fun setupOAuthToken(projectIds: Set<Long>?) {
88+
Mockito.`when`(authenticationFacade.isApiAuthentication).thenReturn(true)
89+
Mockito.`when`(authenticationFacade.oauthTokenCredentials)
90+
.thenReturn(OAuth2TokenCredentials(scopes = emptySet(), projectIds = projectIds))
91+
}
92+
8693
private fun makeRequest(
8794
path: String,
8895
id: String = "",
@@ -141,6 +148,33 @@ class RequestContextServiceTest {
141148
assertThrows<ProjectNotSelectedException> { requestContextService.getTargetProject(reqOldRepo) }
142149
}
143150

151+
@Test
152+
fun `it resolves the implicit project from a single-project OAuth token`() {
153+
setupOAuthToken(setOf(TEST_PROJECT_ID))
154+
155+
val project = requestContextService.getTargetProject(makeRequest("/v2/projects/keys"))
156+
157+
assertThat(project?.id).isEqualTo(TEST_PROJECT_ID)
158+
}
159+
160+
@Test
161+
fun `it throws in implicit scenarios for an all-projects OAuth token`() {
162+
setupOAuthToken(null)
163+
164+
assertThrows<ProjectNotSelectedException> {
165+
requestContextService.getTargetProject(makeRequest("/v2/projects/keys"))
166+
}
167+
}
168+
169+
@Test
170+
fun `it throws in implicit scenarios for a multi-project OAuth token`() {
171+
setupOAuthToken(setOf(TEST_PROJECT_ID, TEST_PROJECT_ID + 1))
172+
173+
assertThrows<ProjectNotSelectedException> {
174+
requestContextService.getTargetProject(makeRequest("/v2/projects/keys"))
175+
}
176+
}
177+
144178
@Test
145179
fun `it correctly detects the current organization`() {
146180
val req = makeRequest("/v2/organizations/{id}/projects", TEST_ORGANIZATION_ID.toString())

0 commit comments

Comments
 (0)