[feat][io] Cassandra sink: support authenticated clusters - #136
Merged
david-streamlio merged 3 commits intoAug 18, 2026
Conversation
### Motivation
The Cassandra sink cannot connect to a cluster that requires authentication.
`CassandraSinkConfig` has no credential fields and `CassandraAbstractSink`
builds its `Cluster` with contact points only:
Cluster.Builder b = Cluster.builder();
...
cluster = b.withoutJMXReporting().build();
Any cluster running `PasswordAuthenticator` — which is the normal production
setting — is therefore unusable with this connector, with no workaround short
of forking it.
This gap was first reported in apache/pulsar#16179 (June 2022), which bundled
the fix with a larger schema-mapping feature. That PR could not be rebased
after PIP-465 moved the connectors out of the core repo, and the
authentication half stands on its own, so it is split out here.
### Modifications
- `CassandraSinkConfig` gains `userName` and `password`, both optional and
`sensitive = true`. No existing field is changed.
- `CassandraAbstractSink.createClient` calls `withCredentials(...)` only when
both are supplied, so an unset pair leaves the connection exactly as it was.
- `open()` loads config through `IOConfigUtils.loadWithSecrets` instead of
`CassandraSinkConfig.load`, so the password can be supplied as a Pulsar
secret rather than sitting in plaintext config. This is the same helper the
kinesis, redis, canal and azure-data-explorer connectors already use. Fields
with no secret configured still come from the config map; the pre-existing
`CassandraStringSinkTest`, which opens the sink with a mocked `SinkContext`,
passes unchanged and covers that.
The sink's write path, the `cassandra` connector registration and
`CassandraStringSink` are untouched.
### Verifying this change
`:cassandra:check` passes. `CassandraSinkAuthTest` adds four cases: credentials
survive config loading, they are null when unconfigured, and the sink writes
successfully both with and without them.
Stated plainly, those do not prove the server rejects an unauthenticated
connection: the test container runs the default `AllowAllAuthenticator`, and
switching it to `PasswordAuthenticator` needs a full version-specific
`cassandra.yaml` override. Enforcement is the server's behaviour; what is this
connector's to get right is that credentials reach the driver and that an unset
pair changes nothing. Happy to add a container with authentication enabled if
reviewers would rather have it.
### Does this pull request potentially affect one of the following parts:
- [ ] Dependencies (add or upgrade a dependency)
- [ ] The public API
- [ ] The schema
- [x] The default values of configurations
- [ ] The threading model
- [ ] The binary protocol
- [ ] The REST endpoints
- [ ] The admin CLI options
- [ ] The metrics
- [x] Anything that affects deployment
**The default values of configurations**: ticked for visibility. No existing
default changes — two new keys are added, both defaulting to unset, where unset
means exactly today's behaviour.
**Deployment**: the module gains `pulsar-io-common`, already in the version
catalog and used by several other connectors. Nothing changes for a deployment
that does not set the new keys.
### Documentation
- [ ] `doc-required`
- [x] `doc-not-needed`
- [ ] `doc`
- [ ] `doc-complete`
The new settings are described by their `@FieldDoc` help text.
…ated connections ### Motivation The tests added with the `userName` / `password` settings assert that credentials survive config loading and that supplying them still yields a working write. They do not assert that the credentials are needed for anything: the test container runs the image default `AllowAllAuthenticator`, so a connection with no credentials is accepted too. That leaves the settings covered only by their own plumbing. Deleting the `withCredentials()` call from `CassandraAbstractSink.createClient()` leaves `CassandraSinkAuthTest` entirely green — all four tests pass with the feature removed. The stated reason for stopping there was that enabling `PasswordAuthenticator` needs a full version-specific `cassandra.yaml`. That is true of the container's `withConfigurationOverride()`, which volume-maps a whole directory over `/etc/cassandra` and so requires carrying every file Cassandra reads from there. Editing the shipped `cassandra.yaml` in place does not. ### Modifications `CassandraSinkAuthEnforcementTest` runs a container whose `cassandra.yaml` has been `sed`-ed to `PasswordAuthenticator` before the entrypoint starts, and drives `CassandraStringSink` against it: - `serverRejectsUnauthenticatedConnection` connects with the driver alone, no sink involved, and requires a refusal. Every other assertion in the class is only worth something while this one holds. - `sinkFailsToOpenWithoutCredentials` and `sinkFailsToOpenWithWrongPassword` require `open()` to be refused, unwrapping an `AuthenticationException` reported either directly or inside a `NoHostAvailableException`. - `sinkWritesWhenCredentialsAreCorrect` requires the configured pair to get past that refusal and the row to land. A `grep -q` after the `sed` makes a substitution that matches nothing fatal: the container exits rather than starting under `AllowAllAuthenticator` and leaving the assertions vacuous. Cassandra 5 nests the setting under `authenticator.class_name`, so a later image tag would otherwise gut this class silently. `CassandraSinkAuthTest` keeps its unauthenticated container and its tests unchanged — that an unset credential pair leaves an existing deployment alone needs a server that does not ask for one. Its scope note now says so and points here instead of stating the gap. ### Verifying this change `:cassandra:check` passes, nine tests. With `withCredentials()` disabled in `CassandraAbstractSink`, `sinkWritesWhenCredentialsAreCorrect` fails while `CassandraSinkAuthTest` stays green, which is the gap this closes. Breaking the `sed` pattern on purpose fails container startup rather than any assertion. The new class ran clean five consecutive times; it costs one additional container.
### Motivation `createClient()` applies credentials only when both `userName` and `password` are set, so a pair with one half missing does not fail — it connects unauthenticated. Against a cluster requiring authentication that surfaces as the driver reporting that no authenticator was configured, which names neither setting and reads like the credentials were never supplied at all rather than like the typo it is. Against a cluster not requiring authentication it succeeds silently, and the operator learns nothing until the cluster is locked down. Half a pair is never a meaningful configuration. Whichever half is present, the intent was to authenticate. ### Modifications `CassandraSinkConfig.validateCredentials()` rejects the pair with an `IllegalArgumentException` naming both settings, and `hasCredentials()` answers whether to authenticate at all. The rule lives on the config rather than in a sink because it is a property of the settings, not of any one sink, and the sinks added on top of this one need the same answer. `CassandraAbstractSink.open()` calls it alongside the existing required-property validation, and so before any connection is attempted. The `@FieldDoc` help on both fields now says they are set together. Tests: `openRejectsUsernameWithoutPassword` and `openRejectsPasswordWithoutUsername` in `CassandraSinkAuthTest`, and `sinkFailsToOpenWithUnknownUser` in `CassandraSinkAuthEnforcementTest`, which completes the credential permutations against an enforcing server — absent, wrong password, unknown user, correct. ### Verifying this change `:cassandra:check` passes, twelve tests. The two new cases point `roots` at a port nothing listens on, so they assert the rejection happens before connecting rather than merely that it happens: with the validation removed they fail with `NoHostAvailableException: ... Cannot connect` instead of passing for the wrong reason.
david-streamlio
force-pushed
the
feat/cassandra-auth
branch
from
August 18, 2026 01:45
2aa7b85 to
fef4728
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
The Cassandra sink cannot connect to a cluster that requires authentication.
CassandraSinkConfighas no credential fields andCassandraAbstractSinkbuilds its
Clusterwith contact points only:Any cluster running
PasswordAuthenticator— which is the normal productionsetting — is therefore unusable with this connector, with no workaround short
of forking it.
This gap was first reported in apache/pulsar#16179 (June 2022), which bundled
the fix with a larger schema-mapping feature. That PR could not be rebased
after PIP-465 moved the connectors out of the core repo, and the
authentication half stands on its own, so it is split out here.
Modifications
CassandraSinkConfiggainsuserNameandpassword, both optional andsensitive = true. No existing field is changed.CassandraAbstractSink.createClientcallswithCredentials(...)only whenboth are supplied, so an unset pair leaves the connection exactly as it was.
open()rejects a pair with only one half set, alongside the existingrequired-property validation and so before any connection is attempted. Half a
pair is never a meaningful configuration — whichever half is present, the intent
was to authenticate — and connecting unauthenticated instead turns a typo into
either a rejection from the server that names neither setting, or a silent
success against a cluster that does not yet require authentication.
open()loads config throughIOConfigUtils.loadWithSecretsinstead ofCassandraSinkConfig.load, so the password can be supplied as a Pulsarsecret rather than sitting in plaintext config. This is the same helper the
kinesis, redis, canal and azure-data-explorer connectors already use. Fields
with no secret configured still come from the config map; the pre-existing
CassandraStringSinkTest, which opens the sink with a mockedSinkContext,passes unchanged and covers that.
The sink's write path, the
cassandraconnector registration andCassandraStringSinkare untouched.Verifying this change
:cassandra:checkpasses, twelve tests across two classes.CassandraSinkAuthTestruns against an unauthenticated cluster — the imagedefault,
AllowAllAuthenticator— and covers the compatibility half: credentialssurvive config loading, they are null when unconfigured, and the sink writes
successfully both with and without them. That last case is the regression this
connector most needs guarded, since an existing unauthenticated deployment must be
unaffected by the settings existing, and it needs a server that does not ask for
credentials in order to mean anything.
CassandraSinkAuthEnforcementTestcovers the other half against a cluster runningPasswordAuthenticator:serverRejectsUnauthenticatedConnectionconnects with the driver alone, no sinkinvolved, and requires a refusal. Every other assertion in the class is only
worth something while this one holds.
sinkFailsToOpenWithoutCredentials,sinkFailsToOpenWithWrongPasswordandsinkFailsToOpenWithUnknownUserrequireopen()to be refused, unwrapping anAuthenticationExceptionreported either directly or inside aNoHostAvailableException.sinkWritesWhenCredentialsAreCorrectrequires the configured pair to get pastthat refusal and the row to land.
An earlier revision of this description said enforcement could not be asserted
without a full version-specific
cassandra.yamloverride. That is true only of thecontainer's
withConfigurationOverride(), which volume-maps a whole directory over/etc/cassandraand so requires carrying every file Cassandra reads from there.Editing the shipped
cassandra.yamlin place before the entrypoint runs does not,and is what this does:
The
grepmakes a substitution that matches nothing fatal — the container exitsrather than starting under
AllowAllAuthenticatorand leaving the assertionsvacuous. Cassandra 5 nests the setting under
authenticator.class_name, so a laterimage tag would otherwise gut the class silently.
openRejectsUsernameWithoutPasswordandopenRejectsPasswordWithoutUsernamepointrootsat a port nothing listens on, so they assert the rejection happens beforeconnecting rather than merely that it happens: with the validation removed they fail
with
NoHostAvailableException: ... Cannot connectinstead of passing for the wrongreason.
Three mutation checks, since a passing test proves nothing on its own. With the
withCredentials()call deleted fromCassandraAbstractSink.createClient(),sinkWritesWhenCredentialsAreCorrectfails whileCassandraSinkAuthTeststaysentirely green — that gap is precisely what the new class closes. Breaking the
sedpattern on purpose fails container startup rather than any assertion. The newclass ran clean five consecutive times; it costs one additional container.
Does this pull request potentially affect one of the following parts:
The default values of configurations: ticked for visibility. No existing
default changes — two new keys are added, both defaulting to unset, where unset
means exactly today's behaviour.
Deployment: the module gains
pulsar-io-common, already in the versioncatalog and used by several other connectors. Nothing changes for a deployment
that does not set the new keys.
Documentation
doc-requireddoc-not-neededdocdoc-completeThe new settings are described by their
@FieldDochelp text.