Skip to content

Commit d0c14cd

Browse files
committed
Add dataset security inheritance logic to Runner creation
- Introduced dataset security inheritance in `RunnerService` to align access control between runners and parameter datasets. - Refactored integration tests to validate inherited dataset security behavior. - Updated test utilities to retrieve and validate dataset parts for additional test scenarios.
1 parent 930f6c7 commit d0c14cd

File tree

13 files changed

+160
-282
lines changed

13 files changed

+160
-282
lines changed

api/src/integrationTest/kotlin/com/cosmotech/api/home/ControllerTestUtils.kt

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import org.springframework.http.MediaType
3131
import org.springframework.mock.web.MockMultipartFile
3232
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf
3333
import org.springframework.test.web.servlet.MockMvc
34+
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
3435
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart
3536
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
3637

@@ -375,6 +376,28 @@ class ControllerTestUtils {
375376
.getString("id")
376377
}
377378

379+
@JvmStatic
380+
fun getDatasetPartsId(
381+
mvc: MockMvc,
382+
organizationId: String,
383+
workspaceId: String,
384+
datasetId: String,
385+
): Array<String> {
386+
val parts =
387+
JSONObject(
388+
mvc.perform(
389+
get(
390+
"/organizations/$organizationId/workspaces/$workspaceId/datasets/$datasetId")
391+
.accept(MediaType.APPLICATION_JSON)
392+
.with(csrf()))
393+
.andReturn()
394+
.response
395+
.contentAsString)
396+
.getJSONArray("parts")
397+
398+
return Array(parts.length()) { i -> parts.getJSONObject(i).getString("id") }
399+
}
400+
378401
@JvmStatic
379402
fun createDatasetPartAndReturnId(
380403
mvc: MockMvc,
@@ -417,6 +440,7 @@ class ControllerTestUtils {
417440

418441
fun constructDatasetCreateRequest(
419442
name: String = DATASET_NAME,
443+
datasetPartName: String = DATASET_PART_NAME,
420444
security: DatasetSecurity? = null
421445
): DatasetCreateRequest {
422446
return DatasetCreateRequest(
@@ -427,7 +451,7 @@ class ControllerTestUtils {
427451
parts =
428452
mutableListOf(
429453
DatasetPartCreateRequest(
430-
name = DATASET_PART_NAME,
454+
name = datasetPartName,
431455
description = DATASET_PART_DESCRIPTION,
432456
tags = mutableListOf("tag_part1", "tag_part2"),
433457
type = DatasetPartTypeEnum.File,

api/src/integrationTest/kotlin/com/cosmotech/api/home/runner/RunnerControllerTests.kt

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import com.cosmotech.api.home.Constants.PLATFORM_ADMIN_EMAIL
99
import com.cosmotech.api.home.ControllerTestBase
1010
import com.cosmotech.api.home.ControllerTestUtils.DatasetUtils.constructDatasetCreateRequest
1111
import com.cosmotech.api.home.ControllerTestUtils.DatasetUtils.createDatasetAndReturnId
12+
import com.cosmotech.api.home.ControllerTestUtils.DatasetUtils.getDatasetPartsId
1213
import com.cosmotech.api.home.ControllerTestUtils.OrganizationUtils.constructOrganizationCreateRequest
1314
import com.cosmotech.api.home.ControllerTestUtils.OrganizationUtils.createOrganizationAndReturnId
1415
import com.cosmotech.api.home.ControllerTestUtils.RunnerUtils.constructRunnerObject
@@ -19,6 +20,7 @@ import com.cosmotech.api.home.ControllerTestUtils.SolutionUtils.createSolutionAn
1920
import com.cosmotech.api.home.ControllerTestUtils.WorkspaceUtils.constructWorkspaceCreateRequest
2021
import com.cosmotech.api.home.ControllerTestUtils.WorkspaceUtils.createWorkspaceAndReturnId
2122
import com.cosmotech.api.home.annotations.WithMockOauth2User
23+
import com.cosmotech.api.home.dataset.DatasetConstants.TEST_FILE_NAME
2224
import com.cosmotech.api.home.organization.OrganizationConstants
2325
import com.cosmotech.api.home.runner.RunnerConstants.NEW_USER_ID
2426
import com.cosmotech.api.home.runner.RunnerConstants.NEW_USER_ROLE
@@ -36,17 +38,17 @@ import com.cosmotech.solution.domain.RunTemplateCreateRequest
3638
import com.cosmotech.solution.domain.RunTemplateParameterCreateRequest
3739
import com.cosmotech.solution.domain.RunTemplateParameterGroupCreateRequest
3840
import com.cosmotech.solution.domain.RunTemplateResourceSizing
41+
import com.cosmotech.workspace.domain.WorkspaceSolution
42+
import com.cosmotech.workspace.domain.WorkspaceUpdateRequest
3943
import com.ninjasquad.springmockk.SpykBean
4044
import io.mockk.every
41-
import org.apache.commons.io.IOUtils
4245
import org.json.JSONObject
4346
import org.junit.jupiter.api.BeforeEach
4447
import org.junit.jupiter.api.Test
4548
import org.slf4j.LoggerFactory
4649
import org.springframework.beans.factory.annotation.Autowired
4750
import org.springframework.boot.test.context.SpringBootTest
4851
import org.springframework.http.MediaType
49-
import org.springframework.mock.web.MockMultipartFile
5052
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document
5153
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf
5254
import org.springframework.test.context.ActiveProfiles
@@ -71,7 +73,7 @@ class RunnerControllerTests : ControllerTestBase() {
7173
private val solutionParameterDefaultValue1 = "this_is_a_default_value"
7274
private val solutionParameterVarType1 = "string"
7375
private val solutionParameterId2 = "param2"
74-
private val solutionParameterDefaultValue2 = "my_folder_test/test.txt"
76+
private val solutionParameterDefaultValue2 = "ignored_value_with_this_varType"
7577

7678
@BeforeEach
7779
fun beforeEach() {
@@ -130,24 +132,29 @@ class RunnerControllerTests : ControllerTestBase() {
130132
workspaceId =
131133
createWorkspaceAndReturnId(
132134
mvc, organizationId, constructWorkspaceCreateRequest(solutionId = solutionId))
133-
datasetId =
134-
createDatasetAndReturnId(mvc, organizationId, workspaceId, constructDatasetCreateRequest())
135135

136-
val fileName = "test.txt"
137-
val fileToUpload =
138-
this::class.java.getResourceAsStream("/solution/$fileName")
139-
?: throw IllegalStateException(
140-
"$fileName file used for organizations/{organization_id}/solutions/{solution_id}/files/POST endpoint documentation cannot be null")
136+
datasetId =
137+
createDatasetAndReturnId(
138+
mvc,
139+
organizationId,
140+
workspaceId,
141+
constructDatasetCreateRequest(datasetPartName = solutionParameterId2))
141142

142-
val mockFile =
143-
MockMultipartFile(
144-
"file", fileName, MediaType.TEXT_PLAIN_VALUE, IOUtils.toByteArray(fileToUpload))
143+
val datasetParts = getDatasetPartsId(mvc, organizationId, workspaceId, datasetId)
145144

146145
mvc.perform(
147-
multipart("/organizations/$organizationId/solutions/$solutionId/files")
148-
.file(mockFile)
149-
.param("overwrite", "true")
150-
.param("destination", "my_folder_test/test.txt")
146+
patch("/organizations/$organizationId/workspaces/$workspaceId")
147+
.contentType(MediaType.APPLICATION_JSON)
148+
.content(
149+
JSONObject(
150+
WorkspaceUpdateRequest(
151+
solution =
152+
WorkspaceSolution(
153+
solutionId = solutionId,
154+
datasetId = datasetId,
155+
defaultParameterValues =
156+
mutableMapOf(solutionParameterId2 to datasetParts[0]))))
157+
.toString())
151158
.accept(MediaType.APPLICATION_JSON)
152159
.with(csrf()))
153160
.andExpect(status().is2xxSuccessful)
@@ -320,9 +327,7 @@ class RunnerControllerTests : ControllerTestBase() {
320327
.andExpect(jsonPath("$.datasets.bases").value(datasetList))
321328
.andExpect(jsonPath("$.datasets.parameters[0].name").value(solutionParameterId2))
322329
.andExpect(jsonPath("$.datasets.parameters[0].type").value(DatasetPartTypeEnum.File.name))
323-
.andExpect(
324-
jsonPath("$.datasets.parameters[0].sourceName")
325-
.value(solutionParameterDefaultValue2.substringAfterLast("/")))
330+
.andExpect(jsonPath("$.datasets.parameters[0].sourceName").value(TEST_FILE_NAME))
326331
.andExpect(jsonPath("$.security.default").value(ROLE_NONE))
327332
.andExpect(jsonPath("$.runSizing.requests.cpu").value("cpu_requests"))
328333
.andExpect(jsonPath("$.runSizing.requests.memory").value("memory_requests"))

api/src/integrationTest/kotlin/com/cosmotech/api/home/solution/SolutionControllerTests.kt

Lines changed: 0 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,12 @@ import com.cosmotech.solution.api.SolutionApiService
2525
import com.cosmotech.solution.domain.*
2626
import io.mockk.every
2727
import io.mockk.mockk
28-
import org.apache.commons.io.IOUtils
2928
import org.json.JSONObject
3029
import org.junit.jupiter.api.BeforeEach
3130
import org.junit.jupiter.api.Test
3231
import org.springframework.beans.factory.annotation.Autowired
3332
import org.springframework.boot.test.context.SpringBootTest
3433
import org.springframework.http.MediaType
35-
import org.springframework.mock.web.MockMultipartFile
3634
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document
3735
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf
3836
import org.springframework.test.context.ActiveProfiles
@@ -1413,135 +1411,4 @@ class SolutionControllerTests : ControllerTestBase() {
14131411
.andDo(
14141412
document("organizations/{organization_id}/solutions/{solution_id}/security/users/GET"))
14151413
}
1416-
1417-
@Test
1418-
@WithMockOauth2User
1419-
fun get_solution_files() {
1420-
1421-
val solutionId =
1422-
createSolutionAndReturnId(mvc, organizationId, constructSolutionCreateRequest())
1423-
1424-
mvc.perform(
1425-
get("/organizations/$organizationId/solutions/$solutionId/files")
1426-
.contentType(MediaType.APPLICATION_JSON))
1427-
.andExpect(status().is2xxSuccessful)
1428-
.andDo(MockMvcResultHandlers.print())
1429-
.andDo(document("organizations/{organization_id}/solutions/{solution_id}/files/GET"))
1430-
}
1431-
1432-
@Test
1433-
@WithMockOauth2User
1434-
fun create_solution_files() {
1435-
val solutionId =
1436-
createSolutionAndReturnId(mvc, organizationId, constructSolutionCreateRequest())
1437-
val fileName = "test.txt"
1438-
val fileToUpload =
1439-
this::class.java.getResourceAsStream("/solution/$fileName")
1440-
?: throw IllegalStateException(
1441-
"$fileName file used for organizations/{organization_id}/solutions/{solution_id}/files/POST endpoint documentation cannot be null")
1442-
1443-
val mockFile =
1444-
MockMultipartFile(
1445-
"file", fileName, MediaType.TEXT_PLAIN_VALUE, IOUtils.toByteArray(fileToUpload))
1446-
1447-
mvc.perform(
1448-
multipart("/organizations/$organizationId/solutions/$solutionId/files")
1449-
.file(mockFile)
1450-
.param("overwrite", "true")
1451-
.param("destination", "path/to/a/directory/")
1452-
.accept(MediaType.APPLICATION_JSON)
1453-
.with(csrf()))
1454-
.andExpect(status().is2xxSuccessful)
1455-
.andExpect(jsonPath("$.fileName").value("path/to/a/directory/$fileName"))
1456-
.andDo(MockMvcResultHandlers.print())
1457-
.andDo(document("organizations/{organization_id}/solutions/{solution_id}/files/POST"))
1458-
}
1459-
1460-
@Test
1461-
@WithMockOauth2User
1462-
fun delete_solution_files() {
1463-
1464-
val solutionId =
1465-
createSolutionAndReturnId(mvc, organizationId, constructSolutionCreateRequest())
1466-
1467-
mvc.perform(
1468-
delete("/organizations/$organizationId/solutions/$solutionId/files")
1469-
.accept(MediaType.APPLICATION_JSON)
1470-
.with(csrf()))
1471-
.andExpect(status().is2xxSuccessful)
1472-
.andDo(MockMvcResultHandlers.print())
1473-
.andDo(document("organizations/{organization_id}/solutions/{solution_id}/files/DELETE"))
1474-
}
1475-
1476-
@Test
1477-
@WithMockOauth2User
1478-
fun delete_solution_file() {
1479-
val solutionId =
1480-
createSolutionAndReturnId(mvc, organizationId, constructSolutionCreateRequest())
1481-
1482-
val fileName = "test.txt"
1483-
val fileToUpload =
1484-
this::class.java.getResourceAsStream("/solution/$fileName")
1485-
?: throw IllegalStateException(
1486-
"$fileName file used for organizations/{organization_id}/solutions/{solution_id}/files/POST endpoint documentation cannot be null")
1487-
1488-
val mockFile =
1489-
MockMultipartFile(
1490-
"file", fileName, MediaType.TEXT_PLAIN_VALUE, IOUtils.toByteArray(fileToUpload))
1491-
1492-
val destination = "path/to/a/directory/"
1493-
mvc.perform(
1494-
multipart("/organizations/$organizationId/solutions/$solutionId/files")
1495-
.file(mockFile)
1496-
.param("overwrite", "true")
1497-
.param("destination", destination)
1498-
.accept(MediaType.APPLICATION_JSON)
1499-
.with(csrf()))
1500-
1501-
mvc.perform(
1502-
delete("/organizations/$organizationId/solutions/$solutionId/files/delete")
1503-
.param("file_name", destination + fileName)
1504-
.accept(MediaType.APPLICATION_JSON)
1505-
.with(csrf()))
1506-
.andExpect(status().is2xxSuccessful)
1507-
.andDo(MockMvcResultHandlers.print())
1508-
.andDo(
1509-
document("organizations/{organization_id}/solutions/{solution_id}/files/delete/DELETE"))
1510-
}
1511-
1512-
@Test
1513-
@WithMockOauth2User
1514-
fun download_solution_file() {
1515-
1516-
val solutionId =
1517-
createSolutionAndReturnId(mvc, organizationId, constructSolutionCreateRequest())
1518-
1519-
val fileName = "test.txt"
1520-
val fileToUpload =
1521-
this::class.java.getResourceAsStream("/solution/$fileName")
1522-
?: throw IllegalStateException(
1523-
"$fileName file used for organizations/{organization_id}/solutions/{solution_id}/files/POST endpoint documentation cannot be null")
1524-
1525-
val mockFile =
1526-
MockMultipartFile(
1527-
"file", fileName, MediaType.TEXT_PLAIN_VALUE, IOUtils.toByteArray(fileToUpload))
1528-
1529-
val destination = "path/to/a/directory/"
1530-
mvc.perform(
1531-
multipart("/organizations/$organizationId/solutions/$solutionId/files")
1532-
.file(mockFile)
1533-
.param("overwrite", "true")
1534-
.param("destination", destination)
1535-
.accept(MediaType.APPLICATION_JSON)
1536-
.with(csrf()))
1537-
1538-
mvc.perform(
1539-
get("/organizations/$organizationId/solutions/$solutionId/files/download")
1540-
.param("file_name", destination + fileName)
1541-
.accept(MediaType.APPLICATION_OCTET_STREAM))
1542-
.andExpect(status().is2xxSuccessful)
1543-
.andDo(MockMvcResultHandlers.print())
1544-
.andDo(
1545-
document("organizations/{organization_id}/solutions/{solution_id}/files/download/GET"))
1546-
}
15471414
}

api/src/integrationTest/resources/solution/test.txt

Lines changed: 0 additions & 1 deletion
This file was deleted.

build.gradle.kts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ version = scmVersion.version
5858

5959
// Dependencies version
6060
val kotlinJvmTarget = 21
61-
val cosmotechApiCommonVersion = "2.1.2-JREY-split_datasetList_on_runner-SNAPSHOT"
61+
val cosmotechApiCommonVersion = "2.1.1-SNAPSHOT"
6262
val redisOmSpringVersion = "0.9.7"
6363
val kotlinCoroutinesVersion = "1.10.2"
6464
val oktaSpringBootVersion = "3.0.7"
@@ -128,7 +128,6 @@ allprojects {
128128
configurations { all { resolutionStrategy { force("com.redis.om:redis-om-spring:0.9.10") } } }
129129

130130
repositories {
131-
mavenLocal()
132131
maven {
133132
name = "GitHubPackages"
134133
url = uri("https://maven.pkg.github.com/Cosmo-Tech/cosmotech-api-common")

dataset/src/integrationTest/kotlin/com/cosmotech/dataset/service/DatasetServiceIntegrationTest.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ class DatasetServiceIntegrationTest() : CsmTestBase() {
296296
}
297297

298298
assertEquals(
299-
"Invalid filename: '$WRONG_ORIGINAL_FILE_NAME'. '..' and '/' are not allowed",
299+
"Invalid filename: '$WRONG_ORIGINAL_FILE_NAME'. File name should neither contains '..' nor starts by '/'.",
300300
exception.message)
301301
}
302302

@@ -1083,7 +1083,7 @@ class DatasetServiceIntegrationTest() : CsmTestBase() {
10831083
type = DatasetPartTypeEnum.File))
10841084
}
10851085
assertEquals(
1086-
"Invalid filename: '$WRONG_ORIGINAL_FILE_NAME'. '..' and '/' are not allowed",
1086+
"Invalid filename: '$WRONG_ORIGINAL_FILE_NAME'. File name should neither contains '..' nor starts by '/'.",
10871087
exception.message)
10881088
}
10891089

@@ -1858,7 +1858,7 @@ class DatasetServiceIntegrationTest() : CsmTestBase() {
18581858
arrayOf(wrongTypeMockMultipartFile))
18591859
}
18601860
assertEquals(
1861-
"Invalid filename: '$WRONG_ORIGINAL_FILE_NAME'. '..' and '/' are not allowed",
1861+
"Invalid filename: '$WRONG_ORIGINAL_FILE_NAME'. File name should neither contains '..' nor starts by '/'.",
18621862
exception.message)
18631863
}
18641864

@@ -2126,7 +2126,7 @@ class DatasetServiceIntegrationTest() : CsmTestBase() {
21262126
datasetPartUpdateRequest)
21272127
}
21282128
assertEquals(
2129-
"Invalid filename: '$WRONG_ORIGINAL_FILE_NAME'. '..' and '/' are not allowed",
2129+
"Invalid filename: '$WRONG_ORIGINAL_FILE_NAME'. File name should neither contains '..' nor starts by '/'.",
21302130
exception.message)
21312131
}
21322132

0 commit comments

Comments
 (0)