diff --git a/allow-list.xml b/allow-list.xml index 6c8d4480..a3a9cd30 100644 --- a/allow-list.xml +++ b/allow-list.xml @@ -48,12 +48,18 @@ + No fix available + ]]> org.springframework:spring-web:5.3.26 CVE-2016-1000027 + + + org.springframework:spring-expression:5.3.26 + CVE-2023-20863 + impl private final EventHolder event; private final ResourceProvider resourceLoader; private final BdkGateway bdk; + private final SharedDataStore sharedDataStore; public CamundaActivityExecutorContext(DelegateExecution execution, T activity, EventHolder event, - ResourceProvider resourceLoader, BdkGateway bdk) { + ResourceProvider resourceLoader, BdkGateway bdk, SharedDataStore sharedDataStore) { this.execution = execution; this.activity = activity; this.event = event; this.resourceLoader = resourceLoader; this.bdk = bdk; + this.sharedDataStore = sharedDataStore; } @Override @@ -211,6 +216,11 @@ public BdkGateway bdk() { return bdk; } + @Override + public SharedDataStore sharedDataStore() { + return sharedDataStore; + } + @Override public T getActivity() { return activity; diff --git a/workflow-bot-app/src/main/java/com/symphony/bdk/workflow/engine/camunda/UtilityFunctionsMapper.java b/workflow-bot-app/src/main/java/com/symphony/bdk/workflow/engine/camunda/UtilityFunctionsMapper.java index a0167d3c..e7627c5c 100644 --- a/workflow-bot-app/src/main/java/com/symphony/bdk/workflow/engine/camunda/UtilityFunctionsMapper.java +++ b/workflow-bot-app/src/main/java/com/symphony/bdk/workflow/engine/camunda/UtilityFunctionsMapper.java @@ -8,6 +8,7 @@ import com.symphony.bdk.gen.api.model.UserV2; import com.symphony.bdk.gen.api.model.V4MessageSent; import com.symphony.bdk.workflow.engine.executor.EventHolder; +import com.symphony.bdk.workflow.engine.executor.SharedDataStore; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.io.JsonStringEncoder; @@ -28,13 +29,20 @@ public class UtilityFunctionsMapper extends FunctionMapper { private static SessionService staticSessionService; + private static SharedDataStore sharedDataStore; + public static void setStaticSessionService(SessionService sessionService) { UtilityFunctionsMapper.staticSessionService = sessionService; } + public static void setSharedStateService(SharedDataStore sharedDataStore) { + UtilityFunctionsMapper.sharedDataStore = sharedDataStore; + } + @Autowired - public UtilityFunctionsMapper(SessionService sessionService) { + public UtilityFunctionsMapper(SessionService sessionService, SharedDataStore sharedDataStore) { setStaticSessionService(sessionService); + setSharedStateService(sharedDataStore); } /** @@ -48,8 +56,10 @@ public UtilityFunctionsMapper(SessionService sessionService) { public static final String MENTIONS = "mentions"; public static final String HASHTAGS = "hashTags"; public static final String CASHTAGS = "cashTags"; - public static final String EMOJIS = "emojis"; - public static final String SESSION = "session"; + public static final String EMOJIS = "emojis"; + public static final String SESSION = "session"; + public static final String READSHARED = "readShared"; + public static final String WRITESHARED = "writeShared"; private static final Map FUNCTION_MAP; private static final ObjectMapper objectMapper = new ObjectMapper(); @@ -64,6 +74,10 @@ public UtilityFunctionsMapper(SessionService sessionService) { FUNCTION_MAP.put(CASHTAGS, ReflectUtil.getMethod(UtilityFunctionsMapper.class, CASHTAGS, Object.class)); FUNCTION_MAP.put(EMOJIS, ReflectUtil.getMethod(UtilityFunctionsMapper.class, ESCAPE, Object.class)); FUNCTION_MAP.put(SESSION, ReflectUtil.getMethod(UtilityFunctionsMapper.class, SESSION)); + FUNCTION_MAP.put(READSHARED, + ReflectUtil.getMethod(UtilityFunctionsMapper.class, READSHARED, String.class, String.class)); + FUNCTION_MAP.put(WRITESHARED, + ReflectUtil.getMethod(UtilityFunctionsMapper.class, WRITESHARED, String.class, String.class, Object.class)); } @Override @@ -83,6 +97,14 @@ public static Object json(String string) { } } + public static Object readShared(String namespace, String key) { + return sharedDataStore.getNamespaceData(namespace).get(key); + } + + public static void writeShared(String namespace, String key, Object data) { + sharedDataStore.putNamespaceData(namespace, key, data); + } + public static String text(String presentationMl) throws PresentationMLParserException { return PresentationMLParser.getTextContent(presentationMl); } diff --git a/workflow-bot-app/src/main/java/com/symphony/bdk/workflow/engine/executor/message/TemplateContentExtractor.java b/workflow-bot-app/src/main/java/com/symphony/bdk/workflow/engine/executor/message/TemplateContentExtractor.java index b435934e..05561226 100644 --- a/workflow-bot-app/src/main/java/com/symphony/bdk/workflow/engine/executor/message/TemplateContentExtractor.java +++ b/workflow-bot-app/src/main/java/com/symphony/bdk/workflow/engine/executor/message/TemplateContentExtractor.java @@ -19,7 +19,8 @@ public static String extractContent(ActivityExecutorContext execution, String } else { Map templateVariables = new HashMap<>(execution.getVariables()); // also bind our utility functions so they can be used inside templates - templateVariables.put(UtilityFunctionsMapper.WDK_PREFIX, new UtilityFunctionsMapper(execution.bdk().session())); + templateVariables.put(UtilityFunctionsMapper.WDK_PREFIX, + new UtilityFunctionsMapper(execution.bdk().session(), execution.sharedDataStore())); if (templatePath != null) { File file = execution.getResourceFile(Path.of(templatePath)); diff --git a/workflow-bot-app/src/main/java/com/symphony/bdk/workflow/shared/DefaultSharedDataStore.java b/workflow-bot-app/src/main/java/com/symphony/bdk/workflow/shared/DefaultSharedDataStore.java new file mode 100644 index 00000000..904a3e00 --- /dev/null +++ b/workflow-bot-app/src/main/java/com/symphony/bdk/workflow/shared/DefaultSharedDataStore.java @@ -0,0 +1,31 @@ +package com.symphony.bdk.workflow.shared; + +import com.symphony.bdk.workflow.engine.executor.SharedDataStore; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Instant; +import java.util.Map; + +@Component +@RequiredArgsConstructor +@Transactional(propagation = Propagation.REQUIRES_NEW) +public class DefaultSharedDataStore implements SharedDataStore { + private final SharedDataRepository repository; + + @Override + public Map getNamespaceData(String namespace) { + return repository.findByNamespace(namespace).orElse(new SharedData()).getProperties(); + } + + @Override + public void putNamespaceData(String namespace, String key, Object data) { + SharedData sharedData = repository.findByNamespace(namespace).orElse(new SharedData().namespace(namespace)); + sharedData.getProperties().put(key, data); + sharedData.setLastUpdated(Instant.now().toEpochMilli()); + repository.save(sharedData); + } +} diff --git a/workflow-bot-app/src/main/java/com/symphony/bdk/workflow/shared/SharedData.java b/workflow-bot-app/src/main/java/com/symphony/bdk/workflow/shared/SharedData.java new file mode 100644 index 00000000..47d0a63a --- /dev/null +++ b/workflow-bot-app/src/main/java/com/symphony/bdk/workflow/shared/SharedData.java @@ -0,0 +1,46 @@ +package com.symphony.bdk.workflow.shared; + +import io.hypersistence.utils.hibernate.type.json.JsonType; +import lombok.Data; +import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; + +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name = "SHARED_STATE_DATA") +@TypeDef(name = "json", typeClass = JsonType.class) +@Data +public class SharedData { + @Id + @GeneratedValue(generator = "system-uuid") + @GenericGenerator(name = "system-uuid", strategy = "uuid2") + @Column(name = "ID") + private String id; + + @NaturalId + @Column(length = 15) + private String namespace; + + @Type(type = "json") + @Column(columnDefinition = "json") + private Map properties = new HashMap<>(); + + @Column(name = "LAST_UPDATED", length = 50) + private Long lastUpdated; + + public SharedData namespace(String namespace) { + this.setNamespace(namespace); + return this; + } + +} diff --git a/workflow-bot-app/src/main/java/com/symphony/bdk/workflow/shared/SharedDataRepository.java b/workflow-bot-app/src/main/java/com/symphony/bdk/workflow/shared/SharedDataRepository.java new file mode 100644 index 00000000..2ffbf812 --- /dev/null +++ b/workflow-bot-app/src/main/java/com/symphony/bdk/workflow/shared/SharedDataRepository.java @@ -0,0 +1,11 @@ +package com.symphony.bdk.workflow.shared; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface SharedDataRepository extends JpaRepository { + Optional findByNamespace(String namespace); +} diff --git a/workflow-bot-app/src/main/java/com/symphony/bdk/workflow/versioning/model/VersionedWorkflow.java b/workflow-bot-app/src/main/java/com/symphony/bdk/workflow/versioning/model/VersionedWorkflow.java index 243538cf..74e9ae88 100644 --- a/workflow-bot-app/src/main/java/com/symphony/bdk/workflow/versioning/model/VersionedWorkflow.java +++ b/workflow-bot-app/src/main/java/com/symphony/bdk/workflow/versioning/model/VersionedWorkflow.java @@ -24,7 +24,7 @@ public class VersionedWorkflow { @Id @GeneratedValue(generator = "system-uuid") - @GenericGenerator(name = "system-uuid", strategy = "uuid") + @GenericGenerator(name = "system-uuid", strategy = "uuid2") @Column(name = "ID") private String id; @Column(name = "WORKFLOW_ID", nullable = false, length = 100) diff --git a/workflow-bot-app/src/main/resources/application-local.yaml b/workflow-bot-app/src/main/resources/application-local.yaml index 7420ebf6..5b6a7192 100644 --- a/workflow-bot-app/src/main/resources/application-local.yaml +++ b/workflow-bot-app/src/main/resources/application-local.yaml @@ -13,6 +13,10 @@ bdk: privateKey: path: xxx +spring: + jpa: + show-sql: true + logging: level: org.camunda.bpm.engine.bpmn.parser: DEBUG @@ -23,3 +27,5 @@ logging: org.camunda.bpm.engine.script: DEBUG org.camunda.bpm: OFF com.symphony.bdk.workflow.engine.camunda.bpmn.CamundaBpmnBuilder: DEBUG + org.hibernate.SQL: DEBUG + org.hibernate.type.descriptor.sql: TRACE diff --git a/workflow-bot-app/src/main/resources/application.yaml b/workflow-bot-app/src/main/resources/application.yaml index 4ccf325b..9bc3eaa8 100644 --- a/workflow-bot-app/src/main/resources/application.yaml +++ b/workflow-bot-app/src/main/resources/application.yaml @@ -51,7 +51,8 @@ spring: driver-class-name: org.h2.Driver jdbc-url: jdbc:h2:mem:process_engine;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE jpa: - show-sql: true + open-in-view: false + show-sql: false hibernate: ddl-auto: update diff --git a/workflow-bot-app/src/test/java/com/symphony/bdk/workflow/SharedDataIntegrationTest.java b/workflow-bot-app/src/test/java/com/symphony/bdk/workflow/SharedDataIntegrationTest.java new file mode 100644 index 00000000..f933f112 --- /dev/null +++ b/workflow-bot-app/src/test/java/com/symphony/bdk/workflow/SharedDataIntegrationTest.java @@ -0,0 +1,70 @@ +package com.symphony.bdk.workflow; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.symphony.bdk.core.service.message.model.Message; +import com.symphony.bdk.gen.api.model.V4Message; +import com.symphony.bdk.workflow.shared.SharedDataRepository; +import com.symphony.bdk.workflow.swadl.SwadlParser; +import com.symphony.bdk.workflow.swadl.v1.Workflow; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.beans.factory.annotation.Autowired; + +public class SharedDataIntegrationTest extends IntegrationTest { + + @Autowired SharedDataRepository sharedDataRepository; + + @Test + @DisplayName("Share counter between process instances") + void shareCounterBetweenProcesses() throws Exception { + final Workflow workflow = + SwadlParser.fromYaml(getClass().getResourceAsStream("/shareddata/shared-counter-data.swadl.yaml")); + final V4Message message = message("/count"); + when(messageService.send(anyString(), any(Message.class))).thenReturn(message); + + engine.deploy(workflow); + engine.onEvent(messageReceived("/count")); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Message.class); + verify(messageService, timeout(5000).times(2)).send(anyString(), captor.capture()); + assertThat(captor.getValue().getContent()).contains("1"); + + engine.onEvent(messageReceived("/count")); + verify(messageService, timeout(5000).times(4)).send(anyString(), captor.capture()); + assertThat(captor.getValue().getContent()).contains("2"); + sharedDataRepository.deleteAll(); + } + + @Test + @DisplayName("Share counter between workflows") + void shareCounterBetweenWorkflows() throws Exception { + final Workflow workflow = + SwadlParser.fromYaml(getClass().getResourceAsStream("/shareddata/shared-counter-data.swadl.yaml")); + final Workflow workflow2 = + SwadlParser.fromYaml(getClass().getResourceAsStream("/shareddata/shared-counter-data2.swadl.yaml")); + final V4Message message = message("/count"); + when(messageService.send(anyString(), any(Message.class))).thenReturn(message); + + engine.deploy(workflow); + engine.deploy(workflow2); + + engine.onEvent(messageReceived("/count")); + ArgumentCaptor captor = ArgumentCaptor.forClass(Message.class); + verify(messageService, timeout(5000).times(2)).send(anyString(), captor.capture()); + assertThat(captor.getValue().getContent()).contains("1"); + + engine.onEvent(messageReceived("/count2")); + verify(messageService, timeout(5000).times(4)).send(anyString(), captor.capture()); + assertThat(captor.getValue().getContent()).contains("2"); + sharedDataRepository.deleteAll(); + } + +} diff --git a/workflow-bot-app/src/test/java/com/symphony/bdk/workflow/utils/UtilityFunctionsMapperTest.java b/workflow-bot-app/src/test/java/com/symphony/bdk/workflow/utils/UtilityFunctionsMapperTest.java index f4673803..e1bfa406 100644 --- a/workflow-bot-app/src/test/java/com/symphony/bdk/workflow/utils/UtilityFunctionsMapperTest.java +++ b/workflow-bot-app/src/test/java/com/symphony/bdk/workflow/utils/UtilityFunctionsMapperTest.java @@ -1,13 +1,19 @@ package com.symphony.bdk.workflow.utils; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.symphony.bdk.core.service.session.SessionService; import com.symphony.bdk.gen.api.model.UserV2; import com.symphony.bdk.workflow.engine.camunda.UtilityFunctionsMapper; +import com.symphony.bdk.workflow.engine.executor.SharedDataStore; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import java.util.Map; @@ -17,7 +23,8 @@ class UtilityFunctionsMapperTest { private static final String BOT_NAME = "BOT NAME"; private static final Long BOT_UID = 1234L; - private final SessionService sessionServiceMock = mock(SessionService.class); + private final SessionService sessionService = mock(SessionService.class); + private final SharedDataStore sharedDataStore = mock(SharedDataStore.class); @Test void jsonStringTest() { @@ -59,13 +66,31 @@ void jsonEmptyTest() { @Test void sessionTest() { - UtilityFunctionsMapper utilityFunctionsMapper = new UtilityFunctionsMapper(this.sessionServiceMock); + UtilityFunctionsMapper.setStaticSessionService(sessionService); UserV2 userV2 = new UserV2().id(BOT_UID).displayName(BOT_NAME); - when(this.sessionServiceMock.getSession()).thenReturn(userV2); + when(this.sessionService.getSession()).thenReturn(userV2); - UserV2 actual = utilityFunctionsMapper.session(); + UserV2 actual = UtilityFunctionsMapper.session(); assertThat(actual.getDisplayName()).isEqualTo(BOT_NAME); assertThat(actual.getId()).isEqualTo(BOT_UID); } + + @Test + @DisplayName("Read shared data method test") + void readSharedTest() { + UtilityFunctionsMapper.setSharedStateService(sharedDataStore); + when(sharedDataStore.getNamespaceData(anyString())).thenReturn(Map.of("key", "value")); + Object actual = UtilityFunctionsMapper.readShared("namespace", "key"); + assertThat(actual).isEqualTo("value"); + } + + @Test + @DisplayName("Write shared data method test") + void writeSharedTest() { + UtilityFunctionsMapper.setSharedStateService(sharedDataStore); + doNothing().when(sharedDataStore).putNamespaceData(eq("namespace"), eq("key"), eq("value")); + UtilityFunctionsMapper.writeShared("namespace", "key", "value"); + verify(sharedDataStore).putNamespaceData(eq("namespace"), eq("key"), eq("value")); + } } diff --git a/workflow-bot-app/src/test/resources/shareddata/shared-counter-data.swadl.yaml b/workflow-bot-app/src/test/resources/shareddata/shared-counter-data.swadl.yaml new file mode 100644 index 00000000..e48019e4 --- /dev/null +++ b/workflow-bot-app/src/test/resources/shareddata/shared-counter-data.swadl.yaml @@ -0,0 +1,19 @@ +id: shared-data + +activities: + - send-message: + id: counter + on: + message-received: + content: /count + content: "increase counter" + - execute-script: + id: vars + script: | + counter = wdk.readShared('test', 'counter') + if(counter == null) counter = 0 + counter++ + wdk.writeShared('test', 'counter', counter) + - send-message: + id: send_counter + content: counter is ${readShared('test', 'counter')} diff --git a/workflow-bot-app/src/test/resources/shareddata/shared-counter-data2.swadl.yaml b/workflow-bot-app/src/test/resources/shareddata/shared-counter-data2.swadl.yaml new file mode 100644 index 00000000..0f2fa262 --- /dev/null +++ b/workflow-bot-app/src/test/resources/shareddata/shared-counter-data2.swadl.yaml @@ -0,0 +1,19 @@ +id: shared-data2 + +activities: + - send-message: + id: counter + on: + message-received: + content: /count2 + content: "increase counter from workflow 2" + - execute-script: + id: vars + script: | + counter = wdk.readShared('test', 'counter') + if(counter == null) counter = 0 + counter++ + wdk.writeShared('test', 'counter', counter) + - send-message: + id: send_counter + content: counter is ${readShared('test', 'counter')} diff --git a/workflow-language/src/main/java/com/symphony/bdk/workflow/engine/executor/ActivityExecutorContext.java b/workflow-language/src/main/java/com/symphony/bdk/workflow/engine/executor/ActivityExecutorContext.java index 21f9a248..f3874498 100644 --- a/workflow-language/src/main/java/com/symphony/bdk/workflow/engine/executor/ActivityExecutorContext.java +++ b/workflow-language/src/main/java/com/symphony/bdk/workflow/engine/executor/ActivityExecutorContext.java @@ -49,6 +49,8 @@ public interface ActivityExecutorContext { */ BdkGateway bdk(); + SharedDataStore sharedDataStore(); + /** * @return The activity definition from the workflow. */ diff --git a/workflow-language/src/main/java/com/symphony/bdk/workflow/engine/executor/SharedDataStore.java b/workflow-language/src/main/java/com/symphony/bdk/workflow/engine/executor/SharedDataStore.java new file mode 100644 index 00000000..520c4dc2 --- /dev/null +++ b/workflow-language/src/main/java/com/symphony/bdk/workflow/engine/executor/SharedDataStore.java @@ -0,0 +1,9 @@ +package com.symphony.bdk.workflow.engine.executor; + +import java.util.Map; + +public interface SharedDataStore { + Map getNamespaceData(String namespace); + + void putNamespaceData(String namespace, String key, Object data); +}