You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by be...@apache.org on 2026/10/23 05:32:51 UTC

(cassandra-sidecar) branch trunk updated: CASSSIDECAR-500: Sanitize/Nullify Cloud Credentials on RestoreJob Completion (#384)

This is an automated email from the ASF dual-hosted git repository.

bbotella pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra-sidecar.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 9f29d60ab CASSSIDECAR-500: Sanitize/Nullify Cloud Credentials on RestoreJob Completion (#384)
9f29d60ab is described below

commit 9f29d60ab2081fb86e96e26bce060476a1fa03bc
Author: Bernardo Botella <bb...@users.noreply.github.com>
AuthorDate: Fri Oct 23 07:32:44 2026 +0200

    CASSSIDECAR-500: Sanitize/Nullify Cloud Credentials on RestoreJob Completion (#384)
    
    Update the job lifecycle manager to automatically nullify or redact sensitive credential fields from the stored payload as soon as a job reaches a terminal state (SUCCEEDED, FAILED, or ABORTED), while preserving the execution logs and metadata for auditing.
    
    Patch by Bernardo Botella; Reviewed by Yifan Cai for CASSSIDECAR-500
---
 CHANGES.txt                                        |  1 +
 .../sidecar/db/RestoreJobDatabaseAccessor.java     | 14 ++++++++
 .../handlers/restore/RestoreJobSummaryHandler.java |  2 +-
 .../sidecar/restore/RestoreJobDiscoverer.java      |  5 +++
 .../db/RestoreJobDatabaseAccessorIntTest.java      | 22 ++++++++++++
 .../restore/RestoreJobSummaryHandlerTest.java      | 42 ++++++++++++++++++++--
 .../sidecar/restore/RestoreJobDiscovererTest.java  | 26 ++++++++++++++
 7 files changed, 108 insertions(+), 4 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 0c2a88f59..bd948e332 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,6 @@
 0.5.0
 -----
+ * Sanitize/Nullify Cloud Credentials on RestoreJob Completion (CASSSIDECAR-500)
  * CDC fails to deserialize nested UDT columns (CASSSIDECAR-507)
  * CDC log_only mode should not require Kafka producer configuration (CASSSIDECAR-492)
  * Report unknown node status as "Unknown" instead of "?" in ring and token range replica responses (CASSSIDECAR-506)
diff --git a/server/src/main/java/org/apache/cassandra/sidecar/db/RestoreJobDatabaseAccessor.java b/server/src/main/java/org/apache/cassandra/sidecar/db/RestoreJobDatabaseAccessor.java
index 1d62a2d1b..5f4529c31 100644
--- a/server/src/main/java/org/apache/cassandra/sidecar/db/RestoreJobDatabaseAccessor.java
+++ b/server/src/main/java/org/apache/cassandra/sidecar/db/RestoreJobDatabaseAccessor.java
@@ -213,6 +213,20 @@ public class RestoreJobDatabaseAccessor extends DatabaseAccessor<RestoreJobsSche
         execute(statement);
     }
 
+    /**
+     * Remove the stored secrets of the restore job.
+     *
+     * @param jobId id of the restore job
+     */
+    public void removeSecrets(UUID jobId)
+    {
+        sidecarSchema.ensureInitialized();
+
+        BoundStatement statement = tableSchema.updateBlobSecrets()
+                                              .bind(RestoreJob.toLocalDate(jobId), jobId, null);
+        execute(statement);
+    }
+
     public RestoreJob find(UUID jobId)
     {
         sidecarSchema.ensureInitialized();
diff --git a/server/src/main/java/org/apache/cassandra/sidecar/handlers/restore/RestoreJobSummaryHandler.java b/server/src/main/java/org/apache/cassandra/sidecar/handlers/restore/RestoreJobSummaryHandler.java
index 3f980aa52..00d318807 100644
--- a/server/src/main/java/org/apache/cassandra/sidecar/handlers/restore/RestoreJobSummaryHandler.java
+++ b/server/src/main/java/org/apache/cassandra/sidecar/handlers/restore/RestoreJobSummaryHandler.java
@@ -85,7 +85,7 @@ public class RestoreJobSummaryHandler extends AbstractHandler<String> implements
         return RoutingContextUtils
         .getAsFuture(context, SC_RESTORE_JOB)
         .compose(restoreJob -> {
-            if (restoreJob.status == null || restoreJob.secrets == null)
+            if (restoreJob.status == null || (!restoreJob.status.isFinal() && restoreJob.secrets == null))
             {
                 logger.error("Restore job record read is missing required fields. job={}", restoreJob);
                 return Future.failedFuture(wrapHttpException(HttpResponseStatus.INTERNAL_SERVER_ERROR,
diff --git a/server/src/main/java/org/apache/cassandra/sidecar/restore/RestoreJobDiscoverer.java b/server/src/main/java/org/apache/cassandra/sidecar/restore/RestoreJobDiscoverer.java
index c49c2fc33..339ba985e 100644
--- a/server/src/main/java/org/apache/cassandra/sidecar/restore/RestoreJobDiscoverer.java
+++ b/server/src/main/java/org/apache/cassandra/sidecar/restore/RestoreJobDiscoverer.java
@@ -529,6 +529,11 @@ public class RestoreJobDiscoverer implements PeriodicTask, RingTopologyChangeLis
 
     private void finalizeJob(RestoreJobManagerGroup restoreJobManagers, RestoreJob job)
     {
+        // Secrets are no longer needed once the job is final.
+        if (job.secrets != null)
+        {
+            restoreJobDatabaseAccessor.removeSecrets(job.jobId);
+        }
         restoreJobManagers.removeJobInternal(job);
         if (job.isManagedBySidecar())
         {
diff --git a/server/src/test/integration/org/apache/cassandra/sidecar/db/RestoreJobDatabaseAccessorIntTest.java b/server/src/test/integration/org/apache/cassandra/sidecar/db/RestoreJobDatabaseAccessorIntTest.java
index 1024f11d1..83bb98719 100644
--- a/server/src/test/integration/org/apache/cassandra/sidecar/db/RestoreJobDatabaseAccessorIntTest.java
+++ b/server/src/test/integration/org/apache/cassandra/sidecar/db/RestoreJobDatabaseAccessorIntTest.java
@@ -167,6 +167,28 @@ class RestoreJobDatabaseAccessorIntTest extends IntegrationTestBase
         assertThat(found.credentialType).isEqualTo(CredentialType.STATIC);
     }
 
+    @CassandraIntegrationTest
+    void testRemoveSecrets()
+    {
+        waitForSchemaReady(30, TimeUnit.SECONDS);
+
+        RestoreJobDatabaseAccessor accessor = injector.getInstance(RestoreJobDatabaseAccessor.class);
+        UUID jobId = createJob(accessor);
+        assertThat(accessor.find(jobId).secrets).isEqualTo(secrets);
+
+        accessor.removeSecrets(jobId);
+        RestoreJob found = accessor.find(jobId);
+        assertThat(found.secrets).isNull();
+        // non-sensitive metadata is preserved for auditing
+        assertThat(found.status).isEqualTo(RestoreJobStatus.CREATED);
+        assertThat(found.credentialType).isEqualTo(CredentialType.STATIC);
+        assertThat(found.jobAgent).isEqualTo("agent");
+
+        // idempotent
+        accessor.removeSecrets(jobId);
+        assertThat(accessor.find(jobId).secrets).isNull();
+    }
+
     private UUID createJob(RestoreJobDatabaseAccessor accessor)
     {
         return createJob(accessor, false);
diff --git a/server/src/test/java/org/apache/cassandra/sidecar/handlers/restore/RestoreJobSummaryHandlerTest.java b/server/src/test/java/org/apache/cassandra/sidecar/handlers/restore/RestoreJobSummaryHandlerTest.java
index 950aa38f6..cce19a125 100644
--- a/server/src/test/java/org/apache/cassandra/sidecar/handlers/restore/RestoreJobSummaryHandlerTest.java
+++ b/server/src/test/java/org/apache/cassandra/sidecar/handlers/restore/RestoreJobSummaryHandlerTest.java
@@ -20,6 +20,7 @@ package org.apache.cassandra.sidecar.handlers.restore;
 
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
 
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -114,6 +115,28 @@ class RestoreJobSummaryHandlerTest extends BaseRestoreJobTests
                                                  context, HttpResponseStatus.INTERNAL_SERVER_ERROR.code());
     }
 
+    @Test
+    void testCompletedJobWithRedactedSecretsSucceeds(VertxTestContext context) throws Throwable
+    {
+        String jobId = "7cd82ff9-d276-11ed-93e5-7fce0df1306f";
+        mockLookupRestoreJob(x -> {
+            UUID id = UUID.fromString(jobId);
+            return RestoreJob.builder()
+                             .createdAt(LocalDate.fromMillisSinceEpoch(UUIDs.unixTimestamp(id)))
+                             .jobId(id).jobAgent("job agent")
+                             .keyspace("ks").table("table")
+                             .jobStatus(RestoreJobStatus.SUCCEEDED)
+                             .jobSecrets(null)
+                             .sstableImportOptions(SSTableImportOptions.defaults())
+                             .build();
+        });
+        sendGetRestoreJobSummaryRequestAndVerify("ks", "table", jobId, context, HttpResponseStatus.OK.code(),
+                                                 response -> {
+                                                     assertThat(response.secrets()).isNull();
+                                                     assertThat(response.status()).isEqualTo(RestoreJobStatus.SUCCEEDED.name());
+                                                 });
+    }
+
     @Test
     void testReadIncompleteRecordFails(VertxTestContext context) throws Throwable
     {
@@ -135,6 +158,21 @@ class RestoreJobSummaryHandlerTest extends BaseRestoreJobTests
                                                           String jobId,
                                                           VertxTestContext context,
                                                           int expectedStatusCode) throws Throwable
+    {
+        sendGetRestoreJobSummaryRequestAndVerify(keyspace, table, jobId, context, expectedStatusCode,
+                                                 response -> {
+                                                     assertThat(response.jobAgent()).isNotNull();
+                                                     assertThat(response.secrets()).isEqualTo(SECRETS);
+                                                     assertThat(response.status()).isNotNull();
+                                                 });
+    }
+
+    private void sendGetRestoreJobSummaryRequestAndVerify(String keyspace,
+                                                          String table,
+                                                          String jobId,
+                                                          VertxTestContext context,
+                                                          int expectedStatusCode,
+                                                          Consumer<RestoreJobSummaryResponsePayload> okAssertions) throws Throwable
     {
         WebClient client = WebClient.create(vertx, new WebClientOptions());
         client.get(server.actualPort(), "localhost", String.format(RESTORE_JOB_INFO_ENDPOINT, keyspace, table, jobId))
@@ -148,9 +186,7 @@ class RestoreJobSummaryHandlerTest extends BaseRestoreJobTests
                           = Json.decodeValue(resp.result().body(), RestoreJobSummaryResponsePayload.class);
                           assertThat(response.keyspace()).isEqualTo(keyspace);
                           assertThat(response.table()).isEqualTo(table);
-                          assertThat(response.jobAgent()).isNotNull();
-                          assertThat(response.secrets()).isEqualTo(SECRETS);
-                          assertThat(response.status()).isNotNull();
+                          okAssertions.accept(response);
                       }
                   })
                   .completeNow();
diff --git a/server/src/test/java/org/apache/cassandra/sidecar/restore/RestoreJobDiscovererTest.java b/server/src/test/java/org/apache/cassandra/sidecar/restore/RestoreJobDiscovererTest.java
index 4404f05a6..598f45b2a 100644
--- a/server/src/test/java/org/apache/cassandra/sidecar/restore/RestoreJobDiscovererTest.java
+++ b/server/src/test/java/org/apache/cassandra/sidecar/restore/RestoreJobDiscovererTest.java
@@ -39,6 +39,7 @@ import io.vertx.core.Promise;
 import org.apache.cassandra.sidecar.TestModule;
 import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
 import org.apache.cassandra.sidecar.common.data.ConsistencyLevel;
+import org.apache.cassandra.sidecar.common.data.RestoreJobSecrets;
 import org.apache.cassandra.sidecar.common.data.RestoreJobStatus;
 import org.apache.cassandra.sidecar.common.response.NodeSettings;
 import org.apache.cassandra.sidecar.common.server.cluster.locator.TokenRange;
@@ -54,6 +55,7 @@ import org.apache.cassandra.sidecar.db.RestoreSliceTest;
 import org.apache.cassandra.sidecar.db.schema.SidecarSchema;
 import org.apache.cassandra.sidecar.exceptions.CassandraUnavailableException;
 import org.apache.cassandra.sidecar.exceptions.RestoreJobExceptions;
+import org.apache.cassandra.sidecar.foundation.RestoreJobSecretsGen;
 import org.apache.cassandra.sidecar.metrics.MetricRegistryFactory;
 import org.apache.cassandra.sidecar.metrics.SidecarMetrics;
 import org.apache.cassandra.sidecar.metrics.SidecarMetricsImpl;
@@ -275,6 +277,30 @@ class RestoreJobDiscovererTest
         assertThat(abortedJobs.getAllValues()).isEqualTo(expectedAbortedJobs);
     }
 
+    @Test
+    void testSecretsRemovedOnlyFromFinalJobsHavingSecrets()
+    {
+        when(sidecarSchema.isInitialized()).thenReturn(true);
+        Date expireAt = new Date(System.currentTimeMillis() + 10000L);
+        RestoreJobSecrets secrets = RestoreJobSecretsGen.genRestoreJobSecrets();
+        List<RestoreJob> jobs = new ArrayList<>();
+        UUID[] finalWithSecrets = { UUIDs.timeBased(), UUIDs.timeBased(), UUIDs.timeBased() };
+        jobs.add(createUpdatedJob(finalWithSecrets[0], "agent", RestoreJobStatus.SUCCEEDED, secrets, expireAt));
+        jobs.add(createUpdatedJob(finalWithSecrets[1], "agent", RestoreJobStatus.FAILED, secrets, expireAt));
+        jobs.add(createUpdatedJob(finalWithSecrets[2], "agent", RestoreJobStatus.ABORTED, secrets, expireAt));
+        // secrets are already removed
+        jobs.add(createUpdatedJob(UUIDs.timeBased(), "agent", RestoreJobStatus.SUCCEEDED, null, expireAt));
+        // not final yet; secrets are still needed
+        jobs.add(createUpdatedJob(UUIDs.timeBased(), "agent", RestoreJobStatus.CREATED, secrets, expireAt));
+        when(mockJobAccessor.findAllRecent(anyLong(), anyInt())).thenReturn(jobs);
+
+        executeBlocking();
+
+        ArgumentCaptor<UUID> removed = ArgumentCaptor.forClass(UUID.class);
+        verify(mockJobAccessor, times(3)).removeSecrets(removed.capture());
+        assertThat(removed.getAllValues()).containsExactlyInAnyOrder(finalWithSecrets);
+    }
+
     @Test
     void testDiscoverSidecarManagedJob() throws Exception
     {


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org