Skip to content

feat: Synchronizer: pluggable CredentialWriter for principal credentials - #259

Open
saidixith002 wants to merge 6 commits into
apache:mainfrom
saidixith002:feat/pluggable-credential-writer
Open

feat: Synchronizer: pluggable CredentialWriter for principal credentials#259
saidixith002 wants to merge 6 commits into
apache:mainfrom
saidixith002:feat/pluggable-credential-writer

Conversation

@saidixith002

Copy link
Copy Markdown
Contributor

Pluggable CredentialWriter for principal credentials

Motivation

polaris-synchronizer currently logs newly generated/rotated principal credentials (clientId / clientSecret) as plaintext directly via slf4j, in two places:

  • create-omnipotent-principal — a one-shot banner logged after creating the omnipotent principal.
  • sync-polaris --sync-principals — logs credentials for every principal created or overwritten on the target during a sync.

This means secrets always land wherever the logger happens to be configured (console, log file, log aggregator, CI output, etc.), with no way to redirect them somewhere safer, and no way to plug in an organization-specific secrets backend without editing tool internals.

What changed

Introduces a small, pluggable CredentialWriter abstraction — mirroring the existing ETagManager / ETagManagerFactory pattern already used in this module for ETag storage — so credential output is configurable instead of hardcoded.

  • CredentialWriter (api/.../access/CredentialWriter.java): generic interface (initialize(properties), writeCredentials(...), AutoCloseable) so the destination is decoupled from the tool.
  • ConsoleCredentialWriter (default): reproduces the existing banner output byte-for-byte — no behavior change for existing invocations.
  • JsonFileCredentialWriter: writes credentials as JSON Lines (one object per principal, flushed immediately) to a file created with owner-only (rw-------) permissions, since it holds plaintext secrets. Supports append for accumulating across runs.
  • CredentialWriterFactory: CONSOLE / FILE / CUSTOM types, with CUSTOM reflectively loading a user-supplied classname — so a future backend (e.g. a secrets manager) can be added with zero changes to this repo, no new CLI flags required.
  • Wired into both create-omnipotent-principal and sync-polaris via two new options: --credential-output-type (default CONSOLE) and --credential-output-properties.
  • PolarisSynchronizer.syncPrincipals() no longer routes secrets through clientLogger — progress logging ("Created principal X on target.") and credential output are now fully separated.

Backward compatibility

Default --credential-output-type=CONSOLE on both commands preserves the exact current banner text, logger name, log level, and placement in program flow. No behavior changes for existing invocations unless the new flags are explicitly used.

Testing

  • New unit tests: CredentialWriterFactoryTest, JsonFileCredentialWriterTest (JSON Lines format, append vs. overwrite semantics, POSIX file permission enforcement).
  • ./gradlew :polaris-synchronizer-api:test :polaris-synchronizer-cli:test passes.
  • Manually verified end-to-end against two local Apache Polaris instances (docker-compose, source + target):
    • create-omnipotent-principal with default CONSOLE output — banner unchanged.
    • create-omnipotent-principal --credential-output-type FILE — file created with 600 permissions, valid JSON line with correct clientId/clientSecret.
    • sync-polaris --sync-principals --credential-output-type FILE — output file accumulates one JSON line per synced principal; confirmed no clientSecret values appear anywhere in clientLogger output.
    • --credential-output-type CUSTOM with a classname — confirmed the reflective loading path works.

Checklist

  • Existing tests pass
  • New tests added for new functionality
  • Documentation (README.md) updated with new flags and examples
  • No behavior change for existing invocations (verified manually)

Replace hardcoded plaintext credential logging in create-omnipotent-principal
and sync-polaris with a pluggable CredentialWriter interface (mirrors the
existing ETagManager pattern), supporting console (default, behavior-
preserving), JSON Lines file output with owner-only permissions, and a
CUSTOM classname escape hatch for future backends without CLI changes.
@dimas-b dimas-b changed the title feat: pluggable CredentialWriter for principal credentials feat: Synchronizer: pluggable CredentialWriter for principal credentials Jul 23, 2026

@dimas-b dimas-b left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice enhancement 👍 Thanks, @saidixith002 !

*
* @param properties properties to configure instance with
*/
void initialize(Map<String, String> properties);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a factory concern. "Users" of CredentialWriter should not need to worry about initializing it. The factory should return a pre-configured instance.

saidixith002 and others added 2 commits July 24, 2026 11:54
Remove initialize() from the CredentialWriter interface so the factory
returns fully pre-configured instances instead of requiring callers to
configure them after construction. Replace the reflective Class.forName
instantiation for the CUSTOM type with java.util.ServiceLoader-based
discovery, matching the pattern used elsewhere in the main Polaris
codebase.

@dimas-b dimas-b left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍 Thanks again, @saidixith002 !

}

return ServiceLoader.load(CredentialWriter.class).stream()
.filter(provider -> provider.type().getName().equals(customWriterClassname))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: do you expect more than one custom class to be available?

implementation("org.slf4j:log4j-over-slf4j:2.0.17")
implementation("org.apache.iceberg:iceberg-spark-runtime-3.3_2.12:1.7.1")
implementation("org.apache.commons:commons-csv:1.13.0")
implementation("com.fasterxml.jackson.core:jackson-databind:2.18.3")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Do you want to use Jackson 3? It looks like it's the future :)

Merging main (which added skipIcebergContent via apache#256) into this branch duplicated the constructor parameter and call-site lists instead of combining them with the credentialWriter parameter already on this branch. Merge them into a single correct signature, update all call sites, and pass a no-op credentialWriter in the skip-iceberg-content tests since they only exercise syncCatalogs().
@saidixith002

Copy link
Copy Markdown
Contributor Author

@dimas-b , Resolved all the conflicts. Please merge

@dimas-b

dimas-b commented Jul 27, 2026

Copy link
Copy Markdown

@saidixith002 : please check CI failure out

saidixith002 and others added 2 commits July 28, 2026 09:10
ServiceLoader provider-config files are plain classnames with no comment
syntax to hold a license header, so Rat was flagging the new CredentialWriter
test service file as an unapproved license, failing the :rat task in CI.
@saidixith002

Copy link
Copy Markdown
Contributor Author

@dimas-b , I'm not able to run the CI workflows. Seems like need approval to run them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants