Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changesetId": "process-timer-admin-v1",
"permissions": [
{
"resourceType": "com.ritense.valtimo.camunda.domain.CamundaTimer",
"action": "complete",
"roleKey": "ROLE_ADMIN"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import com.ritense.valtimo.camunda.authorization.CamundaExecutionSpecificationFa
import com.ritense.valtimo.camunda.authorization.CamundaIdentityLinkSpecificationFactory
import com.ritense.valtimo.camunda.authorization.CamundaProcessDefinitionSpecificationFactory
import com.ritense.valtimo.camunda.authorization.CamundaTaskSpecificationFactory
import com.ritense.valtimo.camunda.authorization.CamundaTimerExecutionMapper
import com.ritense.valtimo.camunda.authorization.CamundaTimerSpecificationFactory
import com.ritense.valtimo.camunda.repository.CamundaBytearrayRepository
import com.ritense.valtimo.camunda.repository.CamundaExecutionRepository
import com.ritense.valtimo.camunda.repository.CamundaHistoricProcessInstanceRepository
Expand Down Expand Up @@ -157,6 +159,13 @@ class ValtimoCamundaAutoConfiguration {
return CamundaExecutionSpecificationFactory(repository, queryDialectHelper)
}

@Bean
@ConditionalOnMissingBean(CamundaTimerSpecificationFactory::class)
@ConditionalOnBean(AuthorizationService::class)
fun camundaTimerSpecificationFactory(): CamundaTimerSpecificationFactory {
return CamundaTimerSpecificationFactory()
}

@Bean
@ConditionalOnMissingBean(CamundaProcessDefinitionSpecificationFactory::class)
@ConditionalOnBean(AuthorizationService::class)
Expand All @@ -172,6 +181,14 @@ class ValtimoCamundaAutoConfiguration {
@ConditionalOnBean(AuthorizationService::class)
fun camundaExecutionProcessDefinitionMapper() = CamundaExecutionProcessDefinitionMapper()

@Bean
@ConditionalOnMissingBean(CamundaTimerExecutionMapper::class)
@ConditionalOnBean(AuthorizationService::class)
fun camundaTimerExecutionMapper(
camundaExecutionRepository: CamundaExecutionRepository
): CamundaTimerExecutionMapper {
return CamundaTimerExecutionMapper(camundaExecutionRepository)
}

@Bean
@ConditionalOnMissingBean(CamundaTaskIdentityLinkMapper::class)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2015-2026 Ritense BV, the Netherlands.
*
* Licensed under EUPL, Version 1.2 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.ritense.valtimo.camunda.authorization

import com.ritense.authorization.Action
import com.ritense.authorization.ResourceActionProvider
import com.ritense.valtimo.camunda.domain.CamundaTimer

class CamundaTimerActionProvider : ResourceActionProvider<CamundaTimer> {
override fun getAvailableActions(): List<Action<CamundaTimer>> {
return listOf(COMPLETE)
}

companion object {
/**
* Completing a timer means firing it ahead of its due date, so the process continues as if
* the timer had elapsed.
*/
@JvmField
val COMPLETE = Action<CamundaTimer>(Action.COMPLETE)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright 2015-2026 Ritense BV, the Netherlands.
*
* Licensed under EUPL, Version 1.2 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.ritense.valtimo.camunda.authorization

import com.ritense.authorization.AuthorizationContext.Companion.runWithoutAuthorization
import com.ritense.authorization.AuthorizationEntityMapper
import com.ritense.authorization.AuthorizationEntityMapperResult
import com.ritense.valtimo.camunda.domain.CamundaExecution
import com.ritense.valtimo.camunda.domain.CamundaTimer
import com.ritense.valtimo.camunda.repository.CamundaExecutionRepository
import jakarta.persistence.criteria.AbstractQuery
import jakarta.persistence.criteria.CriteriaBuilder
import jakarta.persistence.criteria.Root

/**
* Maps a timer onto the process instance it belongs to, so conditions on a timer permission can
* refer to the execution and, through the mappers on the execution, to the case it is part of.
*/
class CamundaTimerExecutionMapper(
private val camundaExecutionRepository: CamundaExecutionRepository,
) : AuthorizationEntityMapper<CamundaTimer, CamundaExecution> {

override fun mapRelated(entity: CamundaTimer): List<CamundaExecution> {
// The process-instance-level execution row has the process instance id as its own id.
return runWithoutAuthorization {
entity.processInstanceId
?.let { camundaExecutionRepository.findById(it).orElse(null) }
?.let { listOf(it) }
?: emptyList()
}
}

override fun mapQuery(
root: Root<CamundaTimer>,
query: AbstractQuery<*>,
criteriaBuilder: CriteriaBuilder
): AuthorizationEntityMapperResult<CamundaExecution> {
throw UnsupportedOperationException("CamundaTimer is not a JPA entity and cannot be queried")
}

override fun supports(fromClass: Class<*>, toClass: Class<*>): Boolean {
return fromClass == CamundaTimer::class.java && toClass == CamundaExecution::class.java
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright 2015-2026 Ritense BV, the Netherlands.
*
* Licensed under EUPL, Version 1.2 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.ritense.valtimo.camunda.authorization

import com.ritense.authorization.permission.Permission
import com.ritense.authorization.request.AuthorizationRequest
import com.ritense.authorization.specification.AuthorizationSpecification
import com.ritense.valtimo.camunda.domain.CamundaTimer
import jakarta.persistence.criteria.AbstractQuery
import jakarta.persistence.criteria.CriteriaBuilder
import jakarta.persistence.criteria.Predicate
import jakarta.persistence.criteria.Root

class CamundaTimerSpecification(
authRequest: AuthorizationRequest<CamundaTimer>,
permissionSupplier: () -> List<Permission>,
) : AuthorizationSpecification<CamundaTimer>(authRequest, permissionSupplier) {

override fun toPredicate(
root: Root<CamundaTimer>,
query: AbstractQuery<*>,
criteriaBuilder: CriteriaBuilder
): Predicate {
throw NotImplementedError("CamundaTimer is not a JPA entity")
}

override fun identifierToEntity(identifier: String): CamundaTimer {
throw NotImplementedError("CamundaTimer is not a JPA entity")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2015-2026 Ritense BV, the Netherlands.
*
* Licensed under EUPL, Version 1.2 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.ritense.valtimo.camunda.authorization

import com.ritense.authorization.permission.Permission
import com.ritense.authorization.request.AuthorizationRequest
import com.ritense.authorization.specification.AuthorizationSpecification
import com.ritense.authorization.specification.AuthorizationSpecificationFactory
import com.ritense.valtimo.camunda.domain.CamundaTimer

class CamundaTimerSpecificationFactory : AuthorizationSpecificationFactory<CamundaTimer> {

override fun create(
request: AuthorizationRequest<CamundaTimer>,
permissionSupplier: () -> List<Permission>
): AuthorizationSpecification<CamundaTimer> {
return CamundaTimerSpecification(request, permissionSupplier)
}

override fun canCreate(request: AuthorizationRequest<*>, permissionSupplier: () -> List<Permission>): Boolean {
return CamundaTimer::class.java == request.resourceType
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2015-2026 Ritense BV, the Netherlands.
*
* Licensed under EUPL, Version 1.2 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.ritense.valtimo.camunda.domain

import org.camunda.bpm.engine.management.JobDefinition
import org.camunda.bpm.engine.runtime.Job
import java.util.Date

/**
* A timer of a running process instance.
*
* Timers are a resource of their own so that permissions on them are independent of permissions on
* the process execution they belong to. Camunda stores all runtime jobs in a single table and only
* exposes them through its own services, so a timer is read from the engine and mapped onto this
* class to evaluate permissions against. Conditions can still refer to the case a timer belongs to
* through the mapper to [CamundaExecution].
*/
class CamundaTimer(
val id: String,
val processInstanceId: String? = null,
val processDefinitionId: String? = null,
val processDefinitionKey: String? = null,
val activityId: String? = null,
val dueDate: Date? = null,
val suspended: Boolean = false,
) {
companion object {
@JvmStatic
fun from(job: Job, jobDefinition: JobDefinition?) = CamundaTimer(
id = job.id,
processInstanceId = job.processInstanceId,
processDefinitionId = job.processDefinitionId,
processDefinitionKey = job.processDefinitionKey,
activityId = jobDefinition?.activityId,
dueDate = job.duedate,
suspended = job.isSuspended,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright 2015-2026 Ritense BV, the Netherlands.
*
* Licensed under EUPL, Version 1.2 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.ritense.valtimo.camunda.authorization

import com.ritense.valtimo.camunda.domain.CamundaExecution
import com.ritense.valtimo.camunda.domain.CamundaProcessDefinition
import com.ritense.valtimo.camunda.domain.CamundaTimer
import com.ritense.valtimo.camunda.repository.CamundaExecutionRepository
import java.util.Optional
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import org.junit.jupiter.api.Test
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever

class CamundaTimerExecutionMapperTest {

private val camundaExecutionRepository: CamundaExecutionRepository = mock()

private val mapper = CamundaTimerExecutionMapper(camundaExecutionRepository)

@Test
fun `should map timer to the execution of its process instance`() {
val execution = mock<CamundaExecution>()
whenever(camundaExecutionRepository.findById("process-instance-id"))
.thenReturn(Optional.of(execution))

val related = mapper.mapRelated(CamundaTimer(id = "job-1", processInstanceId = "process-instance-id"))

assertEquals(listOf(execution), related)
}

@Test
fun `should map to nothing when the execution no longer exists`() {
whenever(camundaExecutionRepository.findById("process-instance-id"))
.thenReturn(Optional.empty())

val related = mapper.mapRelated(CamundaTimer(id = "job-1", processInstanceId = "process-instance-id"))

assertTrue(related.isEmpty())
}

@Test
fun `should map to nothing when the timer has no process instance`() {
val related = mapper.mapRelated(CamundaTimer(id = "job-1"))

assertTrue(related.isEmpty())
}

@Test
fun `should only support mapping a timer to an execution`() {
assertTrue(mapper.supports(CamundaTimer::class.java, CamundaExecution::class.java))
assertFalse(mapper.supports(CamundaTimer::class.java, CamundaProcessDefinition::class.java))
assertFalse(mapper.supports(CamundaExecution::class.java, CamundaExecution::class.java))
}
}
Loading
Loading