Skip to content

Commit c1615e5

Browse files
committed
feat(authorization): Correctly filter repositories
In the endpoint to fetch the repositories of a product, apply a `HierarchyFilter`. Extend `ProductService` accordingly. This makes sure that only repositories are listed that are visible to the user. By having access to some repositories, the user gets implicit READ permission on the owning products. However, in these products, not automatically all repositories are visible. Signed-off-by: Oliver Heger <[email protected]>
1 parent 9f6af7f commit c1615e5

File tree

8 files changed

+161
-9
lines changed

8 files changed

+161
-9
lines changed

core/src/main/kotlin/api/ProductsRoute.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import org.eclipse.apoapsis.ortserver.api.v1.model.PostRepositoryRun
3535
import org.eclipse.apoapsis.ortserver.api.v1.model.Username
3636
import org.eclipse.apoapsis.ortserver.components.authorization.api.ProductRole
3737
import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductPermission
38+
import org.eclipse.apoapsis.ortserver.components.authorization.routes.OrtServerPrincipal.Companion.requirePrincipal
3839
import org.eclipse.apoapsis.ortserver.components.authorization.routes.delete
3940
import org.eclipse.apoapsis.ortserver.components.authorization.routes.get
4041
import org.eclipse.apoapsis.ortserver.components.authorization.routes.mapToModel
@@ -135,9 +136,14 @@ fun Route.products() = route("products/{productId}") {
135136

136137
val productId = call.requireIdParameter("productId")
137138
val pagingOptions = call.pagingOptions(SortProperty("url", SortDirection.ASCENDING))
139+
val principal = requirePrincipal()
138140

139-
val repositoriesForProduct =
140-
productService.listRepositoriesForProduct(productId, pagingOptions.mapToModel(), filter?.mapToModel())
141+
val repositoriesForProduct = productService.listRepositoriesForProductAndUser(
142+
productId,
143+
principal.username,
144+
pagingOptions.mapToModel(),
145+
filter?.mapToModel()
146+
)
141147

142148
val pagedResponse = repositoriesForProduct.mapToApi(Repository::mapToApi)
143149

core/src/test/kotlin/api/DownloadsRouteIntegrationTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ class DownloadsRouteIntegrationTest : AbstractIntegrationTest({
6363
dbExtension.db,
6464
dbExtension.fixtures.productRepository,
6565
dbExtension.fixtures.repositoryRepository,
66-
dbExtension.fixtures.ortRunRepository
66+
dbExtension.fixtures.ortRunRepository,
67+
mockk()
6768
)
6869

6970
val orgId = organizationService.createOrganization(name = "name", description = "description").id

core/src/test/kotlin/api/OrganizationsRouteIntegrationTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,8 @@ class OrganizationsRouteIntegrationTest : AbstractIntegrationTest({
129129
dbExtension.db,
130130
dbExtension.fixtures.productRepository,
131131
dbExtension.fixtures.repositoryRepository,
132-
dbExtension.fixtures.ortRunRepository
132+
dbExtension.fixtures.ortRunRepository,
133+
authorizationService
133134
)
134135
}
135136

core/src/test/kotlin/api/ProductsRouteIntegrationTest.kt

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ import org.eclipse.apoapsis.ortserver.api.v1.model.VulnerabilityForRunsFilters
8181
import org.eclipse.apoapsis.ortserver.api.v1.model.VulnerabilityRating
8282
import org.eclipse.apoapsis.ortserver.components.authorization.api.ProductRole as ApiProductRole
8383
import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductRole
84+
import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryRole
8485
import org.eclipse.apoapsis.ortserver.components.authorization.routes.mapToModel
8586
import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService
8687
import org.eclipse.apoapsis.ortserver.components.authorization.service.DbAuthorizationService
@@ -94,6 +95,7 @@ import org.eclipse.apoapsis.ortserver.model.JobStatus
9495
import org.eclipse.apoapsis.ortserver.model.OrganizationId
9596
import org.eclipse.apoapsis.ortserver.model.OrtRunStatus
9697
import org.eclipse.apoapsis.ortserver.model.ProductId
98+
import org.eclipse.apoapsis.ortserver.model.RepositoryId
9799
import org.eclipse.apoapsis.ortserver.model.RepositoryType
98100
import org.eclipse.apoapsis.ortserver.model.Severity
99101
import org.eclipse.apoapsis.ortserver.model.runs.Identifier
@@ -148,7 +150,8 @@ class ProductsRouteIntegrationTest : AbstractIntegrationTest({
148150
dbExtension.db,
149151
dbExtension.fixtures.productRepository,
150152
dbExtension.fixtures.repositoryRepository,
151-
dbExtension.fixtures.ortRunRepository
153+
dbExtension.fixtures.ortRunRepository,
154+
authorizationService
152155
)
153156

154157
orgId = organizationService.createOrganization(name = "name", description = "description").id
@@ -299,6 +302,71 @@ class ProductsRouteIntegrationTest : AbstractIntegrationTest({
299302
}
300303
}
301304

305+
"return the repositories a user has access to" {
306+
integrationTestApplication {
307+
val createdProduct = createProduct()
308+
309+
val type = RepositoryType.GIT
310+
val url1 = "https://example.com/repo1.git"
311+
val url2 = "https://example.com/repo2.git"
312+
val description = "description"
313+
314+
val createdRepository1 = productService.createRepository(
315+
type = type,
316+
url = url1,
317+
productId = createdProduct.id,
318+
description = description
319+
)
320+
val createdRepository2 = productService.createRepository(
321+
type = type,
322+
url = url2,
323+
productId = createdProduct.id,
324+
description = description
325+
)
326+
productService.createRepository(
327+
type = type,
328+
url = "https://example.com/hidden-repo.git",
329+
productId = createdProduct.id,
330+
description = "You cannot see me"
331+
)
332+
333+
authorizationService.assignRole(
334+
TEST_USER.username.value,
335+
RepositoryRole.READER,
336+
CompoundHierarchyId.forRepository(
337+
OrganizationId(createdProduct.organizationId),
338+
ProductId(createdProduct.id),
339+
RepositoryId(createdRepository1.id)
340+
)
341+
)
342+
authorizationService.assignRole(
343+
TEST_USER.username.value,
344+
RepositoryRole.READER,
345+
CompoundHierarchyId.forRepository(
346+
OrganizationId(createdProduct.organizationId),
347+
ProductId(createdProduct.id),
348+
RepositoryId(createdRepository2.id)
349+
)
350+
)
351+
352+
val response = testUserClient.get("/api/v1/products/${createdProduct.id}/repositories")
353+
354+
response shouldHaveStatus HttpStatusCode.OK
355+
response shouldHaveBody PagedResponse(
356+
listOf(
357+
Repository(createdRepository1.id, orgId, createdProduct.id, type.mapToApi(), url1, description),
358+
Repository(createdRepository2.id, orgId, createdProduct.id, type.mapToApi(), url2, description)
359+
),
360+
PagingData(
361+
limit = DEFAULT_LIMIT,
362+
offset = 0,
363+
totalCount = 2,
364+
sortProperties = listOf(SortProperty("url", SortDirection.ASCENDING))
365+
)
366+
)
367+
}
368+
}
369+
302370
"support query parameters" {
303371
integrationTestApplication {
304372
val createdProduct = createProduct()

core/src/test/kotlin/api/RepositoriesRouteIntegrationTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,8 @@ class RepositoriesRouteIntegrationTest : AbstractIntegrationTest({
143143
dbExtension.db,
144144
dbExtension.fixtures.productRepository,
145145
dbExtension.fixtures.repositoryRepository,
146-
dbExtension.fixtures.ortRunRepository
146+
dbExtension.fixtures.ortRunRepository,
147+
mockk()
147148
)
148149

149150
ortRunRepository = dbExtension.fixtures.ortRunRepository

core/src/test/kotlin/api/RunsRouteIntegrationTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,8 @@ class RunsRouteIntegrationTest : AbstractIntegrationTest({
166166
dbExtension.db,
167167
dbExtension.fixtures.productRepository,
168168
dbExtension.fixtures.repositoryRepository,
169-
dbExtension.fixtures.ortRunRepository
169+
dbExtension.fixtures.ortRunRepository,
170+
mockk()
170171
)
171172

172173
ortRunRepository = dbExtension.fixtures.ortRunRepository

services/hierarchy/src/main/kotlin/ProductService.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,15 @@
1919

2020
package org.eclipse.apoapsis.ortserver.services
2121

22+
import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryRole
23+
import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService
2224
import org.eclipse.apoapsis.ortserver.dao.dbQuery
2325
import org.eclipse.apoapsis.ortserver.dao.repositories.ortrun.OrtRunsTable
2426
import org.eclipse.apoapsis.ortserver.dao.repositories.repository.RepositoriesTable
2527
import org.eclipse.apoapsis.ortserver.model.OrtRun
2628
import org.eclipse.apoapsis.ortserver.model.OrtRunStatus
2729
import org.eclipse.apoapsis.ortserver.model.Product
30+
import org.eclipse.apoapsis.ortserver.model.ProductId
2831
import org.eclipse.apoapsis.ortserver.model.Repository
2932
import org.eclipse.apoapsis.ortserver.model.RepositoryType
3033
import org.eclipse.apoapsis.ortserver.model.repositories.OrtRunRepository
@@ -48,6 +51,7 @@ class ProductService(
4851
private val productRepository: ProductRepository,
4952
private val repositoryRepository: RepositoryRepository,
5053
private val ortRunRepository: OrtRunRepository,
54+
private val authorizationService: AuthorizationService
5155
) {
5256
/**
5357
* Create a repository inside a [product][productId].
@@ -89,6 +93,22 @@ class ProductService(
8993
repositoryRepository.listForProduct(productId, parameters, filter)
9094
}
9195

96+
/**
97+
* List all repositories for a [product][productId] that are visible to a specific [user][userId] according to the
98+
* given [parameters] and [urlFilter].
99+
*/
100+
suspend fun listRepositoriesForProductAndUser(
101+
productId: Long,
102+
userId: String,
103+
parameters: ListQueryParameters = ListQueryParameters.DEFAULT,
104+
urlFilter: FilterParameter? = null
105+
): ListQueryResult<Repository> = getProduct(productId)?.let { product ->
106+
val filter = authorizationService.filterHierarchyIds(userId, RepositoryRole.READER, ProductId(product.id))
107+
db.dbQuery {
108+
repositoryRepository.list(parameters, urlFilter, filter)
109+
}
110+
} ?: ListQueryResult(emptyList(), parameters, 0)
111+
92112
/**
93113
* Update a product by [productId] with the [present][OptionalValue.Present] values.
94114
*/

services/hierarchy/src/test/kotlin/ProductServiceTest.kt

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,26 @@
2020
package org.eclipse.apoapsis.ortserver.services
2121

2222
import io.kotest.core.spec.style.WordSpec
23+
import io.kotest.matchers.collections.beEmpty
2324
import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder
25+
import io.kotest.matchers.should
2426
import io.kotest.matchers.shouldBe
2527

28+
import io.mockk.coEvery
29+
import io.mockk.mockk
30+
31+
import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryRole
32+
import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService
2633
import org.eclipse.apoapsis.ortserver.dao.repositories.ortrun.DaoOrtRunRepository
2734
import org.eclipse.apoapsis.ortserver.dao.repositories.product.DaoProductRepository
2835
import org.eclipse.apoapsis.ortserver.dao.repositories.repository.DaoRepositoryRepository
2936
import org.eclipse.apoapsis.ortserver.dao.test.DatabaseTestExtension
3037
import org.eclipse.apoapsis.ortserver.dao.test.Fixtures
38+
import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId
39+
import org.eclipse.apoapsis.ortserver.model.OrganizationId
40+
import org.eclipse.apoapsis.ortserver.model.ProductId
41+
import org.eclipse.apoapsis.ortserver.model.RepositoryId
42+
import org.eclipse.apoapsis.ortserver.model.util.HierarchyFilter
3143

3244
import org.jetbrains.exposed.sql.Database
3345

@@ -50,7 +62,7 @@ class ProductServiceTest : WordSpec({
5062

5163
"deleteProduct" should {
5264
"delete all repositories associated to this product" {
53-
val service = ProductService(db, productRepository, repositoryRepository, ortRunRepository)
65+
val service = ProductService(db, productRepository, repositoryRepository, ortRunRepository, mockk())
5466

5567
val product = fixtures.createProduct()
5668

@@ -76,7 +88,7 @@ class ProductServiceTest : WordSpec({
7688

7789
"getRepositoryIdsForProduct" should {
7890
"return IDs for all repositories of a product" {
79-
val service = ProductService(db, productRepository, repositoryRepository, ortRunRepository)
91+
val service = ProductService(db, productRepository, repositoryRepository, ortRunRepository, mockk())
8092

8193
val prodId = fixtures.createProduct().id
8294

@@ -87,4 +99,46 @@ class ProductServiceTest : WordSpec({
8799
service.getRepositoryIdsForProduct(prodId).shouldContainExactlyInAnyOrder(repo1Id, repo2Id, repo3Id)
88100
}
89101
}
102+
103+
"listRepositoriesForProductAndUser" should {
104+
"apply a hierarchy filter obtained from the authorization service" {
105+
val userId = "the-test-user"
106+
val repo1 = fixtures.repository
107+
val repo1Id = CompoundHierarchyId.forRepository(
108+
OrganizationId(fixtures.organization.id),
109+
ProductId(fixtures.product.id),
110+
RepositoryId(repo1.id)
111+
)
112+
val repo2 = fixtures.createRepository(url = "https://example.com/another-repo.git")
113+
val repo2Id = CompoundHierarchyId.forRepository(
114+
OrganizationId(fixtures.organization.id),
115+
ProductId(fixtures.product.id),
116+
RepositoryId(repo2.id)
117+
)
118+
119+
val filter = HierarchyFilter(
120+
transitiveIncludes = mapOf(CompoundHierarchyId.REPOSITORY_LEVEL to listOf(repo1Id, repo2Id)),
121+
nonTransitiveIncludes = emptyMap()
122+
)
123+
val authService = mockk<AuthorizationService> {
124+
coEvery {
125+
filterHierarchyIds(userId, RepositoryRole.READER, ProductId(fixtures.product.id))
126+
} returns filter
127+
}
128+
129+
val service = ProductService(db, productRepository, repositoryRepository, ortRunRepository, authService)
130+
val result = service.listRepositoriesForProductAndUser(
131+
productId = fixtures.product.id,
132+
userId = userId
133+
)
134+
135+
result.data shouldContainExactlyInAnyOrder listOf(repo1, repo2)
136+
}
137+
138+
"return an empty list for a non-existing product ID" {
139+
val service = ProductService(db, productRepository, repositoryRepository, ortRunRepository, mockk())
140+
141+
service.listRepositoriesForProductAndUser(-1L, "some-user").data should beEmpty()
142+
}
143+
}
90144
})

0 commit comments

Comments
 (0)