Skip to content

GCP: add pluggable GCS token credential provider - #17468

Open
I-am-aman wants to merge 2 commits into
apache:mainfrom
I-am-aman:gcp-gcs-token-credential-provider
Open

GCP: add pluggable GCS token credential provider#17468
I-am-aman wants to merge 2 commits into
apache:mainfrom
I-am-aman:gcp-gcs-token-credential-provider

Conversation

@I-am-aman

Copy link
Copy Markdown

What

Adds a pluggable storage credential-provider SPI to the GCS FileIO in the iceberg-gcp module:

  • GcsTokenCredentialProvider - interface returning a com.google.auth.oauth2.GoogleCredentials,
    with an initialize(Map<String,String>) hook for provider-specific config.
  • GcsTokenCredentialProviders - factory that loads a custom implementation via DynConstructors
    from the new gcs.token-credential-provider property, with a DefaultGcsTokenCredentialProvider
    backed by Application Default Credentials.
  • New GCPProperties constants (gcs.token-credential-provider,
    gcs.token-credential-provider. prefix), an accessor, and a precondition preventing it from being
    combined with gcs.no-auth.
  • A new branch in PrefixedStorage#credentials(...) that uses the configured provider when present.
  • Unit tests.

This is the GCS analogue of the existing Azure AdlsTokenCredentialProvider (#14136) and AWS's
client.credentials-provider.

Why

The GCS FileIO today supports a static gcs.oauth2.token, gcs.no-auth, native impersonation, and
the vended refresh endpoint - but there is no pluggable way to supply a caller-provided,
self-refreshing source credential
for non-vended setups. None of the existing paths covers this:

  • gcs.oauth2.token is static - it never refreshes, so long-running jobs fail at token expiry.
  • The vended refresh endpoint only refreshes REST-catalog-vended credentials, not a
    caller-supplied source.
  • Native impersonation (gcs.impersonate.service-account) structurally starts from
    GoogleCredentials.getApplicationDefault() as its source credential - there is no property to
    inject an arbitrary caller-supplied source, so it does not cover the bring-your-own-credentials
    case.

This SPI lets integrators plug in a credential source that refreshes, without Iceberg taking on any
specific credential implementation. The default remains Application Default Credentials.

Compatibility

  • Purely additive. Default behaviour is unchanged: with no gcs.token-credential-provider set,
    credential resolution is identical to today (oauth2Token / no-auth / impersonation / ADC).
  • No changes outside iceberg-gcp.

Scope: this is storage-plane auth

GCP has two independent auth planes, and this PR touches only the first:

  • Storage plane - how GCSFileIO / PrefixedStorage authenticate to GCS to read/write data
    files. This is where the new gcs.token-credential-provider SPI lives (properties are gcs.*).
  • Catalog plane - how a REST catalog session authenticates, handled by GoogleAuthManager
    (properties are gcp.auth.*). This PR does not change it.

Testing

  • TestGcsTokenCredentialProviders - default factory, empty/blank provider, custom provider,
    missing no-arg ctor, non-implementing class, prefixed-property extraction.
  • TestGCPProperties - provider property is read; mutual-exclusion with gcs.no-auth is enforced;
    provider + gcs.oauth2.token is allowed.
  • ./gradlew :iceberg-gcp:spotlessCheck :iceberg-gcp:test passes locally.

@github-actions github-actions Bot added the GCP label Aug 1, 2026
} else if (properties.tokenCredentialProvider().isPresent()) {
// A custom provider yields a self-refreshing GoogleCredentials (e.g. built from a
// caller-supplied source credential), addressing static-token expiry for non-vended setups.
return GcsTokenCredentialProviders.from(properties.properties()).credential();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please note a silent auth-precedence trap: in credentials(), branches 1–3 check oauth2Token, noAuth, and impersonateServiceAccount before the new tokenCredentialProvider branch (4), so if a user sets the provider alongside gcs.oauth2.token or gcs.impersonate.service-account, the earlier branch silently wins and the provider is never invoked. Only the gcs.no-auth + provider combination is guarded with Preconditions.checkState. The oauth2 coexistence is documented in testTokenCredentialProviderWithOAuth2Token as intentional ("PrefixedStorage resolves precedence"), but the impersonation case has no guard and no documentation. The impersonation trap is sharpest because impersonation may be inherited from shared catalog config. Recommend either (a) adding a Preconditions.checkState guard mirroring the no-auth one for the impersonation case, or (b) adding explicit precedence Javadoc on GCS_TOKEN_CREDENTIAL_PROVIDER ("ignored if gcs.oauth2.token or gcs.impersonate.service-account is also set") plus a LOG.warn when the provider is set but shadowed. Neither option is currently present.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Going ahead with the second suggestion - precedence Javadoc.

  1. Users may have base configs with gcs.token-credential-provider and environment-specific overrides adding gcs.impersonate.service-account. When merged, both properties coexist. Preconditions.checkState fails at Properties construction time, breaking this legitimate config layering pattern. The override should just "win" at credential selection time, not fail the entire config load.

  2. Selection vs Validation: This is about which credential to use (selection logic in credentials() method at runtime), not whether the config is valid (validation logic in Properties constructor at startup). Multiple auth properties being present isn't a configuration error - it's a selection decision. Preconditions.checkState in the constructor treats it as validation failure when it should just be a runtime precedence rule. Vended path with both oauth2 and credentials provider already sets this precedent.

return NoCredentials.getInstance();
} else if (properties.impersonateServiceAccount().isPresent()) {
return buildImpersonatedCredentials(properties);
} else if (properties.tokenCredentialProvider().isPresent()) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Also, missing PrefixedStorage wiring test: TestGcsTokenCredentialProviders tests the factory in isolation and TestGCPProperties tests property parsing, but no test constructs a PrefixedStorage with gcs.token-credential-provider set and verifies the resulting Storage client receives the provider's GoogleCredentials. TestPrefixedStorage.validParameters already demonstrates the pattern with gcs.oauth2.token (mock credential -> assert setCredentials called). Also missing: a test for the impersonation + provider coexistence behavior (whichever resolution is chosen above). A wiring test is needed to gate confidence in the new branch.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added the tests.

…nd robustness check

Adds precedence documentation, robustness checks (null-gaurd, LOG.warn for shadowed provider), and new tests verifying property parsing and credential selection precedence.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants