Skip to content

Commit 2732dbc

Browse files
maxdmldevhawk
andauthored
support deserializing authenticated roles from other languages (#334)
This path would fail when dequeueing a workflow from another language (python/ts/go store a json array of roles in the database) --------- Co-authored-by: Harry Pierson <harry.pierson@dbos.dev>
1 parent ad05e09 commit 2732dbc

4 files changed

Lines changed: 306 additions & 12 deletions

File tree

transact/src/main/java/dev/dbos/transact/database/WorkflowDAO.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,8 @@ ON CONFLICT (workflow_uuid)
218218
"""
219219
.formatted(this.schema);
220220

221+
var authenticatedRolesJson =
222+
status.authenticatedRoles() != null ? JSONUtil.toJson(status.authenticatedRoles()) : null;
221223
try (PreparedStatement stmt = connection.prepareStatement(insertSQL)) {
222224

223225
var now = Instant.now().toEpochMilli();
@@ -239,7 +241,7 @@ ON CONFLICT (workflow_uuid)
239241

240242
stmt.setString(11, status.authenticatedUser());
241243
stmt.setString(12, status.assumedRole());
242-
stmt.setString(13, status.authenticatedRoles());
244+
stmt.setString(13, authenticatedRolesJson);
243245

244246
stmt.setString(14, status.executorId());
245247
stmt.setString(15, status.appVersion());
@@ -570,23 +572,22 @@ List<WorkflowStatus> listWorkflows(ListWorkflowsInput input) throws SQLException
570572
private static WorkflowStatus resultsToWorkflowStatus(
571573
ResultSet rs, boolean loadInput, boolean loadOutput, DBOSSerializer serializer)
572574
throws SQLException {
573-
var workflow_uuid = rs.getString("workflow_uuid");
574575
String authenticatedRolesJson = rs.getString("authenticated_roles");
575576
String serializedInput = loadInput ? rs.getString("inputs") : null;
576577
String serializedOutput = loadOutput ? rs.getString("output") : null;
577578
String serializedError = loadOutput ? rs.getString("error") : null;
578579
String serialization = loadInput || loadOutput ? rs.getString("serialization") : null;
579580
WorkflowStatus info =
580581
new WorkflowStatus(
581-
workflow_uuid,
582+
rs.getString("workflow_uuid"),
582583
rs.getString("status"),
583584
rs.getString("name"),
584585
Objects.requireNonNullElse(rs.getString("class_name"), ""),
585586
Objects.requireNonNullElse(rs.getString("config_name"), ""),
586587
rs.getString("authenticated_user"),
587588
rs.getString("assumed_role"),
588589
(authenticatedRolesJson != null)
589-
? (String[]) JSONUtil.deserializeToArray(authenticatedRolesJson)
590+
? JSONUtil.fromJson(authenticatedRolesJson, String[].class)
590591
: null,
591592
loadInput
592593
? SerializationUtil.deserializePositionalArgs(
@@ -976,7 +977,7 @@ private static void insertForkedWorkflowStatus(
976977
9,
977978
originalStatus.authenticatedRoles() == null
978979
? null
979-
: JSONUtil.serializeArray(originalStatus.authenticatedRoles()));
980+
: JSONUtil.toJson(originalStatus.authenticatedRoles()));
980981
stmt.setString(10, originalStatus.assumedRole());
981982
stmt.setString(11, Constants.DBOS_INTERNAL_QUEUE);
982983
stmt.setString(

transact/src/main/java/dev/dbos/transact/workflow/internal/WorkflowStatusInternal.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public record WorkflowStatusInternal(
1414
String queuePartitionKey,
1515
String authenticatedUser,
1616
String assumedRole,
17-
String authenticatedRoles,
17+
String[] authenticatedRoles,
1818
String inputs,
1919
String output,
2020
String error,
@@ -79,7 +79,7 @@ public static class Builder {
7979
private String queuePartitionKey;
8080
private String authenticatedUser;
8181
private String assumedRole;
82-
private String authenticatedRoles;
82+
private String[] authenticatedRoles;
8383
private String inputs;
8484
private String output;
8585
private String error;
@@ -154,7 +154,7 @@ public Builder assumedRole(String assumedRole) {
154154
return this;
155155
}
156156

157-
public Builder authenticatedRoles(String authenticatedRoles) {
157+
public Builder authenticatedRoles(String[] authenticatedRoles) {
158158
this.authenticatedRoles = authenticatedRoles;
159159
return this;
160160
}

transact/src/test/java/dev/dbos/transact/database/SystemDatabaseTest.java

Lines changed: 288 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
package dev.dbos.transact.database;
22

3+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
34
import static org.junit.jupiter.api.Assertions.assertEquals;
45
import static org.junit.jupiter.api.Assertions.assertFalse;
6+
import static org.junit.jupiter.api.Assertions.assertNotEquals;
57
import static org.junit.jupiter.api.Assertions.assertNotNull;
68
import static org.junit.jupiter.api.Assertions.assertNull;
79
import static org.junit.jupiter.api.Assertions.assertThrows;
810
import static org.junit.jupiter.api.Assertions.assertTrue;
911

10-
import dev.dbos.transact.DBOS;
1112
import dev.dbos.transact.config.DBOSConfig;
1213
import dev.dbos.transact.exceptions.DBOSMaxRecoveryAttemptsExceededException;
1314
import dev.dbos.transact.exceptions.DBOSQueueDuplicatedException;
@@ -44,7 +45,6 @@ public class SystemDatabaseTest {
4445
DBOSConfig dbosConfig;
4546
@AutoClose SystemDatabase sysdb;
4647

47-
@AutoClose DBOS dbos;
4848
@AutoClose HikariDataSource dataSource;
4949

5050
@BeforeEach
@@ -763,4 +763,290 @@ public void testCreateScheduleWithAllFields() {
763763
assertEquals(ZoneId.of("America/New_York"), s.cronTimezone());
764764
assertEquals("my-queue", s.queueName());
765765
}
766+
767+
@Test
768+
public void testWorkflowStatusAuthenticationFields() throws Exception {
769+
var workflowId = "test-auth-workflow";
770+
var authenticatedUser = "user@example.com";
771+
var assumedRole = "admin";
772+
var authenticatedRoles = new String[] {"admin", "operator", "viewer"};
773+
774+
// Create workflow status with authentication fields
775+
var status =
776+
WorkflowStatusInternal.builder(workflowId, WorkflowState.PENDING)
777+
.name("TestWorkflow")
778+
.className("com.example.TestWorkflow")
779+
.authenticatedUser(authenticatedUser)
780+
.assumedRole(assumedRole)
781+
.authenticatedRoles(authenticatedRoles)
782+
.build();
783+
784+
// Insert into database
785+
sysdb.initWorkflowStatus(status, null, false, false);
786+
787+
// Retrieve via SystemDatabase API and validate object mapping
788+
var retrievedStatus = sysdb.getWorkflowStatus(workflowId);
789+
assertNotNull(retrievedStatus);
790+
assertEquals(authenticatedUser, retrievedStatus.authenticatedUser());
791+
assertEquals(assumedRole, retrievedStatus.assumedRole());
792+
assertArrayEquals(authenticatedRoles, retrievedStatus.authenticatedRoles());
793+
794+
// Validate raw database values using DBUtils
795+
var rawRow = DBUtils.getWorkflowRow(dataSource, workflowId);
796+
assertNotNull(rawRow);
797+
assertEquals(authenticatedUser, rawRow.authenticatedUser());
798+
assertEquals(assumedRole, rawRow.assumedRole());
799+
800+
// Verify the authenticated_roles are stored as JSON in the database
801+
assertEquals("[\"admin\",\"operator\",\"viewer\"]", rawRow.authenticatedRoles());
802+
803+
// Verify other fields are correctly stored
804+
assertEquals(workflowId, rawRow.workflowId());
805+
assertEquals(WorkflowState.PENDING.name(), rawRow.status());
806+
assertEquals("TestWorkflow", rawRow.name());
807+
assertEquals("com.example.TestWorkflow", rawRow.className());
808+
}
809+
810+
@Test
811+
public void testWorkflowStatusAuthenticationFieldsWithNulls() throws Exception {
812+
var workflowId = "test-auth-null-workflow";
813+
814+
// Create workflow status with null authentication fields
815+
var status =
816+
WorkflowStatusInternal.builder(workflowId, WorkflowState.PENDING)
817+
.name("TestNullAuthWorkflow")
818+
.className("com.example.TestNullAuthWorkflow")
819+
.authenticatedUser(null)
820+
.assumedRole(null)
821+
.authenticatedRoles(null)
822+
.build();
823+
824+
// Insert into database
825+
sysdb.initWorkflowStatus(status, null, false, false);
826+
827+
// Retrieve via SystemDatabase API and validate null handling
828+
var retrievedStatus = sysdb.getWorkflowStatus(workflowId);
829+
assertNotNull(retrievedStatus);
830+
assertNull(retrievedStatus.authenticatedUser());
831+
assertNull(retrievedStatus.assumedRole());
832+
assertNull(retrievedStatus.authenticatedRoles());
833+
834+
// Validate raw database values are null
835+
var rawRow = DBUtils.getWorkflowRow(dataSource, workflowId);
836+
assertNotNull(rawRow);
837+
assertNull(rawRow.authenticatedUser());
838+
assertNull(rawRow.assumedRole());
839+
assertNull(rawRow.authenticatedRoles());
840+
}
841+
842+
@Test
843+
public void testWorkflowStatusEmptyAuthenticatedRoles() throws Exception {
844+
var workflowId = "test-auth-empty-roles-workflow";
845+
var authenticatedUser = "user@example.com";
846+
var assumedRole = "basic";
847+
var authenticatedRoles = new String[0]; // Empty array
848+
849+
// Create workflow status with empty authenticated roles
850+
var status =
851+
WorkflowStatusInternal.builder(workflowId, WorkflowState.PENDING)
852+
.name("TestEmptyRolesWorkflow")
853+
.className("com.example.TestEmptyRolesWorkflow")
854+
.authenticatedUser(authenticatedUser)
855+
.assumedRole(assumedRole)
856+
.authenticatedRoles(authenticatedRoles)
857+
.build();
858+
859+
// Insert into database
860+
sysdb.initWorkflowStatus(status, null, false, false);
861+
862+
// Retrieve via SystemDatabase API and validate empty list handling
863+
var retrievedStatus = sysdb.getWorkflowStatus(workflowId);
864+
assertNotNull(retrievedStatus);
865+
assertEquals(authenticatedUser, retrievedStatus.authenticatedUser());
866+
assertEquals(assumedRole, retrievedStatus.assumedRole());
867+
assertEquals(0, retrievedStatus.authenticatedRoles().length);
868+
869+
// Validate raw database values
870+
var rawRow = DBUtils.getWorkflowRow(dataSource, workflowId);
871+
assertNotNull(rawRow);
872+
assertEquals(authenticatedUser, rawRow.authenticatedUser());
873+
assertEquals(assumedRole, rawRow.assumedRole());
874+
assertEquals("[]", rawRow.authenticatedRoles()); // Empty JSON array
875+
}
876+
877+
@Test
878+
public void testForkWorkflowWithAuthenticationFields() throws Exception {
879+
var originalWorkflowId = "test-fork-original-workflow";
880+
var authenticatedUser = "user@example.com";
881+
var assumedRole = "admin";
882+
var authenticatedRoles = new String[] {"admin", "operator", "viewer"};
883+
884+
// Create original workflow status with authentication fields
885+
var originalStatus =
886+
WorkflowStatusInternal.builder(originalWorkflowId, WorkflowState.SUCCESS)
887+
.name("OriginalTestWorkflow")
888+
.className("com.example.OriginalTestWorkflow")
889+
.authenticatedUser(authenticatedUser)
890+
.assumedRole(assumedRole)
891+
.authenticatedRoles(authenticatedRoles)
892+
.build();
893+
894+
// Insert original workflow into database
895+
sysdb.initWorkflowStatus(originalStatus, null, false, false);
896+
897+
// Verify original workflow has correct authentication fields
898+
var originalRetrieved = sysdb.getWorkflowStatus(originalWorkflowId);
899+
assertNotNull(originalRetrieved);
900+
assertEquals(authenticatedUser, originalRetrieved.authenticatedUser());
901+
assertEquals(assumedRole, originalRetrieved.assumedRole());
902+
assertArrayEquals(authenticatedRoles, originalRetrieved.authenticatedRoles());
903+
904+
// Fork the workflow
905+
var forkOptions = new dev.dbos.transact.workflow.ForkOptions(null, "1.0.0", null);
906+
var forkedWorkflowId = sysdb.forkWorkflow(originalWorkflowId, 0, forkOptions);
907+
assertNotNull(forkedWorkflowId);
908+
assertNotEquals(originalWorkflowId, forkedWorkflowId);
909+
910+
// Retrieve forked workflow and validate authentication fields are copied
911+
var forkedStatus = sysdb.getWorkflowStatus(forkedWorkflowId);
912+
assertNotNull(forkedStatus);
913+
assertEquals(authenticatedUser, forkedStatus.authenticatedUser());
914+
assertEquals(assumedRole, forkedStatus.assumedRole());
915+
assertArrayEquals(authenticatedRoles, forkedStatus.authenticatedRoles());
916+
917+
// Verify other forked workflow properties
918+
assertEquals("OriginalTestWorkflow", forkedStatus.name());
919+
assertEquals("com.example.OriginalTestWorkflow", forkedStatus.className());
920+
assertEquals(
921+
WorkflowState.ENQUEUED.name(), forkedStatus.status()); // Forked workflows start as ENQUEUED
922+
assertEquals(originalWorkflowId, forkedStatus.forkedFrom());
923+
924+
// Validate raw database values for both workflows
925+
var originalRawRow = DBUtils.getWorkflowRow(dataSource, originalWorkflowId);
926+
var forkedRawRow = DBUtils.getWorkflowRow(dataSource, forkedWorkflowId);
927+
928+
assertNotNull(originalRawRow);
929+
assertNotNull(forkedRawRow);
930+
931+
// Authentication fields should be identical in raw DB
932+
assertEquals(originalRawRow.authenticatedUser(), forkedRawRow.authenticatedUser());
933+
assertEquals(originalRawRow.assumedRole(), forkedRawRow.assumedRole());
934+
assertEquals(originalRawRow.authenticatedRoles(), forkedRawRow.authenticatedRoles());
935+
assertEquals("[\"admin\",\"operator\",\"viewer\"]", forkedRawRow.authenticatedRoles());
936+
937+
// Verify forked_from field is set correctly
938+
assertNull(originalRawRow.forkedFrom());
939+
assertEquals(originalWorkflowId, forkedRawRow.forkedFrom());
940+
}
941+
942+
@Test
943+
public void testForkWorkflowWithNullAuthenticationFields() throws Exception {
944+
var originalWorkflowId = "test-fork-null-auth-workflow";
945+
946+
// Create original workflow status with null authentication fields
947+
var originalStatus =
948+
WorkflowStatusInternal.builder(originalWorkflowId, WorkflowState.SUCCESS)
949+
.name("NullAuthTestWorkflow")
950+
.className("com.example.NullAuthTestWorkflow")
951+
.authenticatedUser(null)
952+
.assumedRole(null)
953+
.authenticatedRoles(null)
954+
.build();
955+
956+
// Insert original workflow into database
957+
sysdb.initWorkflowStatus(originalStatus, null, false, false);
958+
959+
// Fork the workflow
960+
var forkOptions = new dev.dbos.transact.workflow.ForkOptions(null, "1.0.0", null);
961+
var forkedWorkflowId = sysdb.forkWorkflow(originalWorkflowId, 0, forkOptions);
962+
assertNotNull(forkedWorkflowId);
963+
964+
// Retrieve forked workflow and validate null authentication fields are preserved
965+
var forkedStatus = sysdb.getWorkflowStatus(forkedWorkflowId);
966+
assertNotNull(forkedStatus);
967+
assertNull(forkedStatus.authenticatedUser());
968+
assertNull(forkedStatus.assumedRole());
969+
assertNull(forkedStatus.authenticatedRoles());
970+
971+
// Validate raw database values
972+
var originalRawRow = DBUtils.getWorkflowRow(dataSource, originalWorkflowId);
973+
var forkedRawRow = DBUtils.getWorkflowRow(dataSource, forkedWorkflowId);
974+
975+
assertNotNull(originalRawRow);
976+
assertNotNull(forkedRawRow);
977+
978+
// Null authentication fields should be preserved
979+
assertNull(originalRawRow.authenticatedUser());
980+
assertNull(forkedRawRow.authenticatedUser());
981+
assertNull(originalRawRow.assumedRole());
982+
assertNull(forkedRawRow.assumedRole());
983+
assertNull(originalRawRow.authenticatedRoles());
984+
assertNull(forkedRawRow.authenticatedRoles());
985+
}
986+
987+
@Test
988+
public void testForkWorkflowWithEmptyAuthenticatedRoles() throws Exception {
989+
var originalWorkflowId = "test-fork-empty-auth-roles-workflow";
990+
var authenticatedUser = "user@example.com";
991+
var assumedRole = "basic";
992+
var authenticatedRoles = new String[0]; // Empty array
993+
994+
// Create original workflow status with empty authenticated roles
995+
var originalStatus =
996+
WorkflowStatusInternal.builder(originalWorkflowId, WorkflowState.SUCCESS)
997+
.name("EmptyAuthRolesTestWorkflow")
998+
.className("com.example.EmptyAuthRolesTestWorkflow")
999+
.authenticatedUser(authenticatedUser)
1000+
.assumedRole(assumedRole)
1001+
.authenticatedRoles(authenticatedRoles)
1002+
.build();
1003+
1004+
// Insert original workflow into database
1005+
sysdb.initWorkflowStatus(originalStatus, null, false, false);
1006+
1007+
// Verify original workflow has correct authentication fields including empty roles
1008+
var originalRetrieved = sysdb.getWorkflowStatus(originalWorkflowId);
1009+
assertNotNull(originalRetrieved);
1010+
assertEquals(authenticatedUser, originalRetrieved.authenticatedUser());
1011+
assertEquals(assumedRole, originalRetrieved.assumedRole());
1012+
assertEquals(0, originalRetrieved.authenticatedRoles().length);
1013+
1014+
// Fork the workflow
1015+
var forkOptions = new dev.dbos.transact.workflow.ForkOptions(null, "1.0.0", null);
1016+
var forkedWorkflowId = sysdb.forkWorkflow(originalWorkflowId, 0, forkOptions);
1017+
assertNotNull(forkedWorkflowId);
1018+
assertNotEquals(originalWorkflowId, forkedWorkflowId);
1019+
1020+
// Retrieve forked workflow and validate empty authentication roles are preserved
1021+
var forkedStatus = sysdb.getWorkflowStatus(forkedWorkflowId);
1022+
assertNotNull(forkedStatus);
1023+
assertEquals(authenticatedUser, forkedStatus.authenticatedUser());
1024+
assertEquals(assumedRole, forkedStatus.assumedRole());
1025+
assertEquals(0, forkedStatus.authenticatedRoles().length);
1026+
1027+
// Verify other forked workflow properties
1028+
assertEquals("EmptyAuthRolesTestWorkflow", forkedStatus.name());
1029+
assertEquals("com.example.EmptyAuthRolesTestWorkflow", forkedStatus.className());
1030+
assertEquals(WorkflowState.ENQUEUED.name(), forkedStatus.status());
1031+
assertEquals(originalWorkflowId, forkedStatus.forkedFrom());
1032+
1033+
// Validate raw database values for both workflows
1034+
var originalRawRow = DBUtils.getWorkflowRow(dataSource, originalWorkflowId);
1035+
var forkedRawRow = DBUtils.getWorkflowRow(dataSource, forkedWorkflowId);
1036+
1037+
assertNotNull(originalRawRow);
1038+
assertNotNull(forkedRawRow);
1039+
1040+
// Empty authentication roles should be stored as empty JSON array and preserved in fork
1041+
assertEquals(authenticatedUser, originalRawRow.authenticatedUser());
1042+
assertEquals(authenticatedUser, forkedRawRow.authenticatedUser());
1043+
assertEquals(assumedRole, originalRawRow.assumedRole());
1044+
assertEquals(assumedRole, forkedRawRow.assumedRole());
1045+
assertEquals("[]", originalRawRow.authenticatedRoles()); // Empty JSON array
1046+
assertEquals("[]", forkedRawRow.authenticatedRoles()); // Empty JSON array preserved in fork
1047+
1048+
// Verify forked_from field is set correctly
1049+
assertNull(originalRawRow.forkedFrom());
1050+
assertEquals(originalWorkflowId, forkedRawRow.forkedFrom());
1051+
}
7661052
}

0 commit comments

Comments
 (0)