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