Skip to content

Commit 28784fc

Browse files
committed
chore(security): Handle ownerId where it makes sense
This includes endpoints that create/register something, or updates that are liable to change resource ownerships.
1 parent 47c767f commit 28784fc

File tree

9 files changed

+158
-26
lines changed

9 files changed

+158
-26
lines changed

common/src/main/kotlin/com/cosmotech/api/exceptions/CsmClientExceptions.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,9 @@ class CsmResourceNotFoundException(
1414
override val message: String,
1515
override val cause: Throwable? = null
1616
) : CsmClientException(message, cause)
17+
18+
@ResponseStatus(HttpStatus.FORBIDDEN)
19+
class CsmAccessForbiddenException(
20+
override val message: String,
21+
override val cause: Throwable? = null
22+
) : CsmClientException(message, cause)

common/src/main/kotlin/com/cosmotech/api/utils/SecurityUtils.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
// Licensed under the MIT license.
33
package com.cosmotech.api.utils
44

5+
import java.lang.IllegalStateException
6+
import org.springframework.security.core.Authentication
57
import org.springframework.security.core.context.SecurityContextHolder
68

7-
fun getCurrentUserId(): String = SecurityContextHolder.getContext().authentication.name
9+
fun getCurrentAuthentication(): Authentication? = SecurityContextHolder.getContext().authentication
10+
11+
fun getCurrentUserName(): String? = getCurrentAuthentication()?.name
12+
13+
fun getCurrentAuthenticatedUserName() =
14+
getCurrentUserName()
15+
?: throw IllegalStateException("User Authentication not found in Security Context")

connector/src/main/kotlin/com/cosmotech/connector/azure/ConnectorServiceImpl.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import com.cosmotech.api.azure.AbstractCosmosBackedService
77
import com.cosmotech.api.azure.findAll
88
import com.cosmotech.api.azure.findByIdOrThrow
99
import com.cosmotech.api.events.ConnectorRemoved
10-
import com.cosmotech.api.utils.getCurrentUserId
10+
import com.cosmotech.api.exceptions.CsmAccessForbiddenException
11+
import com.cosmotech.api.utils.getCurrentAuthenticatedUserName
1112
import com.cosmotech.connector.api.ConnectorApiService
1213
import com.cosmotech.connector.domain.Connector
1314
import javax.annotation.PostConstruct
@@ -36,11 +37,17 @@ class ConnectorServiceImpl : AbstractCosmosBackedService(), ConnectorApiService
3637
override fun registerConnector(connector: Connector): Connector =
3738
cosmosTemplate.insert(
3839
coreConnectorContainer,
39-
connector.copy(id = idGenerator.generate("connector"), ownerId = getCurrentUserId()))
40+
connector.copy(
41+
id = idGenerator.generate("connector"), ownerId = getCurrentAuthenticatedUserName()))
4042
?: throw IllegalStateException("No connector returned in response: $connector")
4143

4244
override fun unregisterConnector(connectorId: String) {
43-
cosmosTemplate.deleteEntity(coreConnectorContainer, this.findConnectorById(connectorId))
45+
val connector = this.findConnectorById(connectorId)
46+
if (connector.ownerId != getCurrentAuthenticatedUserName()) {
47+
// TODO Only the owner or an admin should be able to perform this operation
48+
throw CsmAccessForbiddenException("You are not allowed to delete this Resource")
49+
}
50+
cosmosTemplate.deleteEntity(coreConnectorContainer, connector)
4451
this.eventPublisher.publishEvent(ConnectorRemoved(this, connectorId))
4552
}
4653
}

dataset/src/main/kotlin/com/cosmotech/dataset/azure/DatasetServiceImpl.kt

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import com.cosmotech.api.events.ConnectorRemoved
1313
import com.cosmotech.api.events.ConnectorRemovedForOrganization
1414
import com.cosmotech.api.events.OrganizationRegistered
1515
import com.cosmotech.api.events.OrganizationUnregistered
16+
import com.cosmotech.api.exceptions.CsmAccessForbiddenException
1617
import com.cosmotech.api.utils.changed
18+
import com.cosmotech.api.utils.getCurrentAuthenticatedUserName
1719
import com.cosmotech.api.utils.toDomain
1820
import com.cosmotech.connector.api.ConnectorApiService
1921
import com.cosmotech.dataset.api.DatasetApiService
@@ -60,7 +62,9 @@ class DatasetServiceImpl(
6062
val existingConnector = connectorService.findConnectorById(dataset.connector!!.id!!)
6163
logger.debug("Found connector: {}", existingConnector)
6264

63-
val datasetCopy = dataset.copy(id = idGenerator.generate("dataset"))
65+
val datasetCopy =
66+
dataset.copy(
67+
id = idGenerator.generate("dataset"), ownerId = getCurrentAuthenticatedUserName())
6468
datasetCopy.connector!!.apply {
6569
name = existingConnector.name
6670
version = existingConnector.version
@@ -70,14 +74,30 @@ class DatasetServiceImpl(
7074
}
7175

7276
override fun deleteDataset(organizationId: String, datasetId: String) {
73-
cosmosTemplate.deleteEntity(
74-
"${organizationId}_datasets", findDatasetById(organizationId, datasetId))
77+
val dataset = findDatasetById(organizationId, datasetId)
78+
if (dataset.ownerId != getCurrentAuthenticatedUserName()) {
79+
// TODO Only the owner or an admin should be able to perform this operation
80+
throw CsmAccessForbiddenException("You are not allowed to delete this Resource")
81+
}
82+
cosmosTemplate.deleteEntity("${organizationId}_datasets", dataset)
7583
}
7684

7785
override fun updateDataset(organizationId: String, datasetId: String, dataset: Dataset): Dataset {
7886
val existingDataset = findDatasetById(organizationId, datasetId)
7987

8088
var hasChanged = false
89+
90+
if (dataset.ownerId != null && dataset.changed(existingDataset) { ownerId }) {
91+
// Allow to change the ownerId as well, but only the owner can transfer the ownership
92+
if (existingDataset.ownerId != getCurrentAuthenticatedUserName()) {
93+
// TODO Only the owner or an admin should be able to perform this operation
94+
throw CsmAccessForbiddenException(
95+
"You are not allowed to change the ownership of this Resource")
96+
}
97+
existingDataset.ownerId = dataset.ownerId
98+
hasChanged = true
99+
}
100+
81101
if (dataset.name != null && dataset.changed(existingDataset) { name }) {
82102
existingDataset.name = dataset.name
83103
hasChanged = true
@@ -87,11 +107,20 @@ class DatasetServiceImpl(
87107
hasChanged = true
88108
}
89109
if (dataset.connector != null && dataset.changed(existingDataset) { connector }) {
90-
// TODO Validate connector ID
110+
// Validate connector ID
111+
if (dataset.connector?.id.isNullOrBlank()) {
112+
throw IllegalArgumentException("Connector ID is null or blank")
113+
}
114+
val existingConnector = connectorService.findConnectorById(dataset.connector!!.id!!)
115+
logger.debug("Found connector: {}", existingConnector)
116+
91117
existingDataset.connector = dataset.connector
118+
existingDataset.connector!!.apply {
119+
name = existingConnector.name
120+
version = existingConnector.version
121+
}
92122
hasChanged = true
93123
}
94-
// TODO Allow to change the ownerId as well, but only the owner can transfer the ownership
95124

96125
if (dataset.tags != null && dataset.tags?.toSet() != existingDataset.tags?.toSet()) {
97126
existingDataset.tags = dataset.tags

organization/src/main/kotlin/com/cosmotech/organization/azure/OrganizationServiceImpl.kt

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import com.cosmotech.api.azure.AbstractCosmosBackedService
99
import com.cosmotech.api.azure.findAll
1010
import com.cosmotech.api.azure.findByIdOrThrow
1111
import com.cosmotech.api.events.*
12+
import com.cosmotech.api.exceptions.CsmAccessForbiddenException
1213
import com.cosmotech.api.utils.changed
14+
import com.cosmotech.api.utils.getCurrentAuthenticatedUserName
1315
import com.cosmotech.api.utils.toDomain
1416
import com.cosmotech.organization.api.OrganizationApiService
1517
import com.cosmotech.organization.domain.Organization
@@ -111,12 +113,13 @@ class OrganizationServiceImpl(private val userService: UserApiService) :
111113
val usersWithNames =
112114
usersLoaded?.let { organization.users?.map { it.copy(name = usersLoaded[it.id]!!.name!!) } }
113115

114-
// TODO Set the ownerID to the logged-in user
115-
116116
val organizationRegistered =
117117
cosmosTemplate.insert(
118118
coreOrganizationContainer,
119-
organization.copy(id = newOrganizationId, users = usersWithNames))
119+
organization.copy(
120+
id = newOrganizationId,
121+
users = usersWithNames,
122+
ownerId = getCurrentAuthenticatedUserName()))
120123

121124
val organizationId =
122125
organizationRegistered.id
@@ -165,7 +168,13 @@ class OrganizationServiceImpl(private val userService: UserApiService) :
165168
}
166169

167170
override fun unregisterOrganization(organizationId: String) {
168-
cosmosTemplate.deleteEntity(coreOrganizationContainer, findOrganizationById(organizationId))
171+
val organization = findOrganizationById(organizationId)
172+
if (organization.ownerId != getCurrentAuthenticatedUserName()) {
173+
// TODO Only the owner or an admin should be able to perform this operation
174+
throw CsmAccessForbiddenException("You are not allowed to delete this Resource")
175+
}
176+
177+
cosmosTemplate.deleteEntity(coreOrganizationContainer, organization)
169178

170179
this.eventPublisher.publishEvent(OrganizationUnregistered(this, organizationId))
171180

@@ -179,11 +188,22 @@ class OrganizationServiceImpl(private val userService: UserApiService) :
179188
val existingOrganization = findOrganizationById(organizationId)
180189

181190
var hasChanged = false
191+
192+
if (organization.ownerId != null && organization.changed(existingOrganization) { ownerId }) {
193+
// Allow to change the ownerId as well, but only the owner can transfer the ownership
194+
if (existingOrganization.ownerId != getCurrentAuthenticatedUserName()) {
195+
// TODO Only the owner or an admin should be able to perform this operation
196+
throw CsmAccessForbiddenException(
197+
"You are not allowed to change the ownership of this Resource")
198+
}
199+
existingOrganization.ownerId = organization.ownerId
200+
hasChanged = true
201+
}
202+
182203
if (organization.name != null && organization.changed(existingOrganization) { name }) {
183204
existingOrganization.name = organization.name
184205
hasChanged = true
185206
}
186-
// TODO Allow to change the ownerId as well, but only the owner can transfer the ownership
187207

188208
var userIdsRemoved: List<String>? = listOf()
189209
if (organization.users != null) {

scenario/src/main/kotlin/com/cosmotech/scenario/azure/ScenarioServiceImpl.kt

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ package com.cosmotech.scenario.azure
55
import com.azure.cosmos.models.*
66
import com.cosmotech.api.azure.AbstractCosmosBackedService
77
import com.cosmotech.api.events.*
8+
import com.cosmotech.api.exceptions.CsmAccessForbiddenException
89
import com.cosmotech.api.utils.changed
910
import com.cosmotech.api.utils.convertToMap
11+
import com.cosmotech.api.utils.getCurrentAuthenticatedUserName
1012
import com.cosmotech.api.utils.toDomain
1113
import com.cosmotech.organization.api.OrganizationApiService
1214
import com.cosmotech.scenario.api.ScenarioApiService
@@ -156,6 +158,7 @@ class ScenarioServiceImpl(
156158
val scenarioToSave =
157159
scenario.copy(
158160
id = idGenerator.generate("scenario"),
161+
ownerId = getCurrentAuthenticatedUserName(),
159162
solutionId = solutionId,
160163
solutionName = solutionName,
161164
creationDate = now,
@@ -189,9 +192,14 @@ class ScenarioServiceImpl(
189192
}
190193

191194
override fun deleteScenario(organizationId: String, workspaceId: String, scenarioId: String) {
192-
cosmosTemplate.deleteEntity(
193-
"${organizationId}_scenario_data",
194-
this.findScenarioById(organizationId, workspaceId, scenarioId))
195+
val scenario = this.findScenarioById(organizationId, workspaceId, scenarioId)
196+
197+
if (scenario.ownerId != getCurrentAuthenticatedUserName()) {
198+
// TODO Only the owner or an admin should be able to perform this operation
199+
throw CsmAccessForbiddenException("You are not allowed to delete this Resource")
200+
}
201+
202+
cosmosTemplate.deleteEntity("${organizationId}_scenario_data", scenario)
195203
// TODO Notify users
196204
}
197205

@@ -302,6 +310,18 @@ class ScenarioServiceImpl(
302310
val workspace = workspaceService.findWorkspaceById(organizationId, workspaceId)
303311

304312
var hasChanged = false
313+
314+
if (scenario.ownerId != null && scenario.changed(existingScenario) { ownerId }) {
315+
// Allow to change the ownerId as well, but only the owner can transfer the ownership
316+
if (existingScenario.ownerId != getCurrentAuthenticatedUserName()) {
317+
// TODO Only the owner or an admin should be able to perform this operation
318+
throw CsmAccessForbiddenException(
319+
"You are not allowed to change the ownership of this Resource")
320+
}
321+
existingScenario.ownerId = scenario.ownerId
322+
hasChanged = true
323+
}
324+
305325
if (scenario.name != null && scenario.changed(existingScenario) { name }) {
306326
existingScenario.name = scenario.name
307327
hasChanged = true

scenariorun/src/main/kotlin/com/cosmotech/scenariorun/ScenarioRunServiceImpl.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ package com.cosmotech.scenariorun
55
import com.azure.cosmos.models.*
66
import com.cosmotech.api.argo.WorkflowUtils
77
import com.cosmotech.api.azure.AbstractCosmosBackedService
8+
import com.cosmotech.api.exceptions.CsmAccessForbiddenException
89
import com.cosmotech.api.utils.convertToMap
10+
import com.cosmotech.api.utils.getCurrentAuthenticatedUserName
911
import com.cosmotech.api.utils.toDomain
1012
import com.cosmotech.connector.api.ConnectorApiService
1113
import com.cosmotech.dataset.api.DatasetApiService
@@ -79,8 +81,12 @@ class ScenariorunServiceImpl(
7981
}
8082

8183
override fun deleteScenarioRun(organizationId: String, scenariorunId: String) {
82-
cosmosTemplate.deleteEntity(
83-
"${organizationId}_scenario_data", this.findScenarioRunById(organizationId, scenariorunId))
84+
val scenarioRun = this.findScenarioRunById(organizationId, scenariorunId)
85+
if (scenarioRun.ownerId != getCurrentAuthenticatedUserName()) {
86+
// TODO Only the owner or an admin should be able to perform this operation
87+
throw CsmAccessForbiddenException("You are not allowed to delete this Resource")
88+
}
89+
cosmosTemplate.deleteEntity("${organizationId}_scenario_data", scenarioRun)
8490
}
8591

8692
override fun findScenarioRunById(organizationId: String, scenariorunId: String): ScenarioRun =
@@ -326,6 +332,7 @@ class ScenariorunServiceImpl(
326332
val scenarioRun =
327333
ScenarioRun(
328334
id = idGenerator.generate("scenariorun", prependPrefix = "SR-"),
335+
ownerId = getCurrentAuthenticatedUserName(),
329336
csmSimulationRun = csmSimulationId,
330337
organizationId = organizationId,
331338
workspaceId = workspaceId,

solution/src/main/kotlin/com/cosmotech/solution/azure/SolutionServiceImpl.kt

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import com.cosmotech.api.azure.findByIdOrThrow
1010
import com.cosmotech.api.azure.sanitizeForAzureStorage
1111
import com.cosmotech.api.events.OrganizationRegistered
1212
import com.cosmotech.api.events.OrganizationUnregistered
13+
import com.cosmotech.api.exceptions.CsmAccessForbiddenException
1314
import com.cosmotech.api.exceptions.CsmResourceNotFoundException
1415
import com.cosmotech.api.utils.changed
16+
import com.cosmotech.api.utils.getCurrentAuthenticatedUserName
1517
import com.cosmotech.solution.api.SolutionApiService
1618
import com.cosmotech.solution.domain.*
1719
import org.apache.commons.compress.archivers.ArchiveException
@@ -123,12 +125,18 @@ class SolutionServiceImpl(
123125
override fun createSolution(organizationId: String, solution: Solution) =
124126
cosmosTemplate.insert(
125127
"${organizationId}_solutions",
126-
solution.copy(id = idGenerator.generate("solution", prependPrefix = "SOL-")))
128+
solution.copy(
129+
id = idGenerator.generate("solution", prependPrefix = "SOL-"),
130+
ownerId = getCurrentAuthenticatedUserName()))
127131
?: throw IllegalArgumentException("No solution returned in response: $solution")
128132

129133
override fun deleteSolution(organizationId: String, solutionId: String) {
130-
cosmosTemplate.deleteEntity(
131-
"${organizationId}_solutions", findSolutionById(organizationId, solutionId))
134+
val solution = findSolutionById(organizationId, solutionId)
135+
if (solution.ownerId != getCurrentAuthenticatedUserName()) {
136+
// TODO Only the owner or an admin should be able to perform this operation
137+
throw CsmAccessForbiddenException("You are not allowed to delete this Resource")
138+
}
139+
cosmosTemplate.deleteEntity("${organizationId}_solutions", solution)
132140
}
133141

134142
override fun deleteSolutionRunTemplate(
@@ -153,6 +161,16 @@ class SolutionServiceImpl(
153161
val existingSolution = findSolutionById(organizationId, solutionId)
154162

155163
var hasChanged = false
164+
if (solution.ownerId != null && solution.changed(existingSolution) { ownerId }) {
165+
// Allow to change the ownerId as well, but only the owner can transfer the ownership
166+
if (existingSolution.ownerId != getCurrentAuthenticatedUserName()) {
167+
// TODO Only the owner or an admin should be able to perform this operation
168+
throw CsmAccessForbiddenException(
169+
"You are not allowed to change the ownership of this Resource")
170+
}
171+
existingSolution.ownerId = solution.ownerId
172+
hasChanged = true
173+
}
156174
if (solution.key != null && solution.changed(existingSolution) { key }) {
157175
existingSolution.key = solution.key
158176
hasChanged = true
@@ -180,8 +198,6 @@ class SolutionServiceImpl(
180198
hasChanged = true
181199
}
182200

183-
// TODO Allow to change the ownerId as well, but only the owner can transfer the ownership
184-
185201
if (solution.tags != null && solution.tags?.toSet() != existingSolution.tags?.toSet()) {
186202
existingSolution.tags = solution.tags
187203
hasChanged = true

0 commit comments

Comments
 (0)