Skip to content

Commit 5bba125

Browse files
committed
feat(authorization): Correctly filter products
In the endpoint to fetch the products of an organization, apply a `HierarchyFilter`. Extend `OrganizationService` accordingly. This makes sure that only products are listed that are visible to the user. If a user has only been granted access to specific repositories, he or she should only see the products these repositories belong to, even if there is an implicit READ right on the organization. Signed-off-by: Oliver Heger <[email protected]>
1 parent 9441e5e commit 5bba125

File tree

4 files changed

+138
-1
lines changed

4 files changed

+138
-1
lines changed

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import org.eclipse.apoapsis.ortserver.api.v1.model.Username
3838
import org.eclipse.apoapsis.ortserver.components.authorization.api.OrganizationRole
3939
import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationPermission
4040
import org.eclipse.apoapsis.ortserver.components.authorization.routes.OrtServerPrincipal
41+
import org.eclipse.apoapsis.ortserver.components.authorization.routes.OrtServerPrincipal.Companion.requirePrincipal
4142
import org.eclipse.apoapsis.ortserver.components.authorization.routes.delete
4243
import org.eclipse.apoapsis.ortserver.components.authorization.routes.get
4344
import org.eclipse.apoapsis.ortserver.components.authorization.routes.mapToModel
@@ -167,10 +168,12 @@ fun Route.organizations() = route("organizations") {
167168
val orgId = call.requireIdParameter("organizationId")
168169
val pagingOptions = call.pagingOptions(SortProperty("name", SortDirection.ASCENDING))
169170
val filter = call.filterParameter("filter")
171+
val principal = requirePrincipal()
170172

171173
val productsForOrganization =
172-
organizationService.listProductsForOrganization(
174+
organizationService.listProductsForOrganizationAndUser(
173175
orgId,
176+
principal.username,
174177
pagingOptions.mapToModel(),
175178
filter?.mapToModel()
176179
)

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

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ import org.eclipse.apoapsis.ortserver.api.v1.model.VulnerabilityForRunsFilters
6767
import org.eclipse.apoapsis.ortserver.api.v1.model.VulnerabilityRating
6868
import org.eclipse.apoapsis.ortserver.components.authorization.api.OrganizationRole as ApiOrganizationRole
6969
import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationRole
70+
import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductRole
71+
import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryRole
7072
import org.eclipse.apoapsis.ortserver.components.authorization.routes.mapToModel
7173
import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService
7274
import org.eclipse.apoapsis.ortserver.components.authorization.service.DbAuthorizationService
@@ -75,6 +77,8 @@ import org.eclipse.apoapsis.ortserver.core.TEST_USER
7577
import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId
7678
import org.eclipse.apoapsis.ortserver.model.JobStatus
7779
import org.eclipse.apoapsis.ortserver.model.OrganizationId
80+
import org.eclipse.apoapsis.ortserver.model.ProductId
81+
import org.eclipse.apoapsis.ortserver.model.RepositoryId
7882
import org.eclipse.apoapsis.ortserver.model.RepositoryType
7983
import org.eclipse.apoapsis.ortserver.model.Severity
8084
import org.eclipse.apoapsis.ortserver.model.runs.Identifier
@@ -605,6 +609,77 @@ class OrganizationsRouteIntegrationTest : AbstractIntegrationTest({
605609
}
606610
}
607611

612+
"return only products for which the user has ProductPermission.READ" {
613+
integrationTestApplication {
614+
val createdOrganization = createOrganization()
615+
val orgId = createdOrganization.id
616+
val otherOrg = createOrganization("otherOrg")
617+
618+
val name1 = "name1"
619+
val name2 = "name2"
620+
val description = "description"
621+
622+
val createdProduct1 =
623+
organizationService.createProduct(name = name1, description = description, organizationId = orgId)
624+
val createdProduct2 =
625+
organizationService.createProduct(name = name2, description = description, organizationId = orgId)
626+
organizationService.createProduct(name = "name3", description = description, organizationId = orgId)
627+
val createdRepo = productService.createRepository(
628+
RepositoryType.GIT,
629+
"https://example.com/repo.git",
630+
createdProduct2.id,
631+
null
632+
)
633+
val productInOtherOrg = organizationService.createProduct(
634+
name = "otherOrgProduct",
635+
description = description,
636+
organizationId = otherOrg.id
637+
)
638+
639+
authorizationService.assignRole(
640+
TEST_USER.username.value,
641+
ProductRole.READER,
642+
CompoundHierarchyId.forProduct(
643+
OrganizationId(createdOrganization.id),
644+
ProductId(createdProduct1.id)
645+
)
646+
)
647+
authorizationService.assignRole(
648+
TEST_USER.username.value,
649+
ProductRole.READER,
650+
CompoundHierarchyId.forProduct(
651+
OrganizationId(otherOrg.id),
652+
ProductId(productInOtherOrg.id)
653+
)
654+
)
655+
authorizationService.assignRole(
656+
TEST_USER.username.value,
657+
RepositoryRole.WRITER,
658+
CompoundHierarchyId.forRepository(
659+
OrganizationId(createdOrganization.id),
660+
ProductId(createdProduct2.id),
661+
RepositoryId(createdRepo.id)
662+
)
663+
)
664+
665+
val response = testUserClient.get("/api/v1/organizations/$orgId/products")
666+
667+
response shouldHaveStatus HttpStatusCode.OK
668+
response shouldHaveBody PagedResponse(
669+
listOf(
670+
Product(createdProduct1.id, orgId, name1, description),
671+
Product(createdProduct2.id, orgId, name2, description)
672+
),
673+
PagingData(
674+
limit = DEFAULT_LIMIT,
675+
offset = 0,
676+
totalCount = 2,
677+
sortProperties = listOf(SortProperty("name", SortDirection.ASCENDING))
678+
)
679+
)
680+
}
681+
}
682+
608683
"support query parameters" {
609684
integrationTestApplication {
610685
val orgId = createOrganization().id

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

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

2222
import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationRole
23+
import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductRole
2324
import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService
2425
import org.eclipse.apoapsis.ortserver.dao.dbQuery
2526
import org.eclipse.apoapsis.ortserver.dao.repositories.product.ProductsTable
2627
import org.eclipse.apoapsis.ortserver.dao.repositories.repository.RepositoriesTable
2728
import org.eclipse.apoapsis.ortserver.model.Organization
29+
import org.eclipse.apoapsis.ortserver.model.OrganizationId
30+
import org.eclipse.apoapsis.ortserver.model.Product
2831
import org.eclipse.apoapsis.ortserver.model.repositories.OrganizationRepository
2932
import org.eclipse.apoapsis.ortserver.model.repositories.ProductRepository
3033
import org.eclipse.apoapsis.ortserver.model.util.FilterParameter
@@ -115,6 +118,25 @@ class OrganizationService(
115118
productRepository.listForOrganization(organizationId, parameters, filter)
116119
}
117120

121+
/**
122+
* List all products for an [organization][organizationId] that are visible to the user with the given [userId],
123+
* applying the given [parameters] and optional [nameFilter].
124+
*/
125+
suspend fun listProductsForOrganizationAndUser(
126+
organizationId: Long,
127+
userId: String,
128+
parameters: ListQueryParameters = ListQueryParameters.DEFAULT,
129+
nameFilter: FilterParameter? = null
130+
): ListQueryResult<Product> {
131+
val productFilter = authorizationService.filterHierarchyIds(
132+
userId,
133+
ProductRole.READER,
134+
OrganizationId(organizationId)
135+
)
136+
137+
return productRepository.list(parameters, nameFilter, productFilter)
138+
}
139+
118140
/**
119141
* Update an organization by [organizationId] with the [present][OptionalValue.Present] values.
120142
*/

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,15 @@ import io.mockk.coEvery
2727
import io.mockk.mockk
2828

2929
import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationRole
30+
import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductRole
3031
import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService
3132
import org.eclipse.apoapsis.ortserver.dao.repositories.organization.DaoOrganizationRepository
3233
import org.eclipse.apoapsis.ortserver.dao.repositories.product.DaoProductRepository
3334
import org.eclipse.apoapsis.ortserver.dao.test.DatabaseTestExtension
3435
import org.eclipse.apoapsis.ortserver.dao.test.Fixtures
3536
import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId
3637
import org.eclipse.apoapsis.ortserver.model.OrganizationId
38+
import org.eclipse.apoapsis.ortserver.model.ProductId
3739
import org.eclipse.apoapsis.ortserver.model.util.HierarchyFilter
3840

3941
import org.jetbrains.exposed.sql.Database
@@ -97,4 +99,39 @@ class OrganizationServiceTest : WordSpec({
9799
organizations.data shouldContainExactlyInAnyOrder listOf(fixtures.organization, org2)
98100
}
99101
}
102+
103+
"listOrganizationsForUserAndOrganization" should {
104+
"filter for products visible to a specific user in a specific organization" {
105+
val userId = "test-user"
106+
val org1Id = fixtures.organization.id
107+
val prod1 = fixtures.createProduct("product", organizationId = org1Id)
108+
val prod2 = fixtures.createProduct("product2", organizationId = org1Id)
109+
fixtures.createProduct("hiddenProduct")
110+
val prod1HierarchyId = CompoundHierarchyId.forProduct(
111+
OrganizationId(org1Id),
112+
ProductId(prod1.id)
113+
)
114+
val prod2HierarchyId = CompoundHierarchyId.forProduct(
115+
OrganizationId(org1Id),
116+
ProductId(prod2.id)
117+
)
118+
119+
val authService = mockk<AuthorizationService> {
120+
coEvery {
121+
filterHierarchyIds(userId, ProductRole.READER, OrganizationId(org1Id))
122+
} returns HierarchyFilter(
123+
transitiveIncludes = mapOf(
124+
CompoundHierarchyId.PRODUCT_LEVEL to listOf(prod1HierarchyId, prod2HierarchyId)
125+
),
126+
nonTransitiveIncludes = emptyMap()
127+
)
128+
}
129+
130+
val service = OrganizationService(db, organizationRepository, productRepository, authService)
131+
val products = service.listProductsForOrganizationAndUser(org1Id, userId)
132+
133+
products.totalCount shouldBe 2
134+
products.data shouldContainExactlyInAnyOrder listOf(prod1, prod2)
135+
}
136+
}
100137
})

0 commit comments

Comments
 (0)