feat(spark): add OIDC federation token provider to the UC connector - #3
Draft
parisni wants to merge 6 commits into
Draft
feat(spark): add OIDC federation token provider to the UC connector#3parisni wants to merge 6 commits into
parisni wants to merge 6 commits into
Conversation
The connector could only authenticate with a static token or an OAuth client-credentials secret. Both are password-equivalents that have to be stored and rotated; a Kubernetes workload can instead prove its identity with the service account token the kubelet already projects for it. Adds a third provider performing the RFC 8693 token exchange over the JDK HTTP client, consistent with the 0.2.x backport constraints. The subject token is re-read on every renewal rather than cached at construction time, since the kubelet rotates the projected file and a Spark session outlives it. Follows the existing flat option formalism (`oidc.*` alongside `oauth.*`) rather than upstream 0.3.x `auth.type`, to keep this a contained addition. Existing modes keep working and win on precedence.
The providers were selected by probing which option keys were present, and only exposed accessToken(). That shape cannot survive a serialization boundary, which 0.3.x needs: its executors rebuild a provider from the Hadoop configuration to renew vended credentials off the driver, via TokenProvider.configs() and a type-keyed dispatch. Adopts that contract instead of the flat key probing: initialize(configs), configs(), and dispatch on `type` with support for a custom provider class name. AuthConfigUtils normalizes the catalog options, backported from 0.3.x and extended to infer a type from the un-prefixed keys this connector already shipped, so existing catalog configurations keep working untouched. The OIDC provider gains a configs() that carries only the token file path, never a secret, so the map is safe to propagate. The file is still read wherever the provider is rebuilt, keeping the kubelet rotation behaviour.
configs() had no caller outside its own tests. It exists in 0.3.x so an executor can rebuild a provider from the Hadoop configuration and renew UC-vended storage credentials, which needs the connectors/hadoop module this connector does not have. Backporting that module was considered and dropped: it depends on client internals absent from 0.2.x, and the jobs run with skipCredentialVending, so nothing is vended and the executors reach S3 through the ambient IAM chain. Everything the connector authenticates stays on the driver, since UCSingleCatalog is a TableCatalog resolved at planning time. The type-based dispatch and AuthConfigUtils are kept: UCSingleCatalog uses them, and they bring the auth.* keys plus the custom-provider escape hatch.
The type-based dispatch inferred an auth type from the mere presence of a key, so a declared-but-blank credential selected a provider that then rejected it. Spark hands over the keys a session declared even when their value is empty, which is how an unauthenticated local metastore is configured, and the whole TableReadWriteTest suite broke on "Configuration key 'token' is missing or empty". The probing this replaced tested the value, not the key, so the regression came in with the dispatch. Blank now counts as unset everywhere: the auth.* copy, the legacy token key and the legacy group inference.
S3 was the one cloud whose vended credentials went into the plan as literal fs.s3a.access.key/secret.key/session.token. They last about an hour, so any stage still running past that point failed on S3 rather than on anything that looked like an auth problem. Azure and GCS already went through a token provider; only S3 held raw keys. Adds an S3A credentials provider that re-requests credentials from Unity Catalog before they expire, keyed by the table scope. Installed on the executors too, which is why the UC coordinates and the auth configuration travel in the Hadoop configuration: the driver's provider instance does not cross the plan, so each side re-authenticates on its own. The cache is static because generateCredentialProps sets fs.s3a.impl.disable.cache=true: Spark rebuilds the FileSystem, and the provider with it, on every resolution, so an instance-level cache would never be reused. Collapses the 0.3.x AwsVendedTokenProvider and its GenericCredentialProvider base into one class: that split exists to share a cache across four clouds and five credential scopes, none of which applies to a table-scoped S3-only path. The 0.3.x module itself cannot be backported, as it needs client internals absent from 0.2.x. Unused while skipCredentialVending stays true, which is the current setting: it makes turning vending on a configuration change rather than a code change.
The javadoc tool crashes on the generated sources with a ClientCodeException wrapping "StringIndexOutOfBoundsException: begin 6470, end 7150, length 0", which failed client/publishM2 and left the spark-leboncoin image build without a client jar to copy. It reproduced on amd64 while arm64 built the same sources, so the javadoc pass is the fragile part, not the code. The jar consumed by the connector needs no javadoc, so drop the artifact rather than working around a JDK bug on generated code we do not own.
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.
Why
The 0.2.x Spark connector could only authenticate with a static
tokenor an OAuthclient-credentials secret. Both are password-equivalents that must be stored and rotated. A
Kubernetes workload can instead prove its identity with the service account token the kubelet
already projects for it, so nothing secret is stored on either side.
What
OIDC federation provider
FileOidcUCTokenProvider: RFC 8693 token exchange over the JDK HTTP client, consistent withthe 0.2.x backport constraints (no
RetryingApiClient/Clockavailable here). Token cached andrenewed 30s before expiry, thread-safe via double-checked locking, like
OAuthUCTokenProvider.rotates the projected file and a Spark session outlives a single token.
Authorizationheader on the exchange — the subject token is the proof, there is no secret.oidc.uri,oidc.clientId,oidc.tokenFilePath.Alignment with the 0.3.x option formalism
UCTokenProvidergainsinitialize(Map)andcreatedispatches on atypekey (static,oauth,oidc, or a fully qualified custom provider class name), mirroring 0.3.x including thecustom-class escape hatch.
AuthConfigsholds the key constants, named exactly as upstream, so a future rebase is a packagemove rather than a rename.
AuthConfigUtilsis backported to normalize catalog options, strip theauth.prefix and reject akey configured twice. Extended beyond upstream to infer a
typefrom the un-prefixed keys thisconnector already shipped, so existing configurations keep working untouched.
UCSingleCatalogdelegates toAuthConfigUtilsand keys off the presence oftype.Renewable S3 vended credentials
S3 was the one cloud whose vended credentials landed in the plan as literal
fs.s3a.access.key/secret.key/session.token. They last about an hour, so a stage outliving themfailed on S3 rather than on anything resembling an auth problem. Azure and GCS already went through a
token provider.
S3VendedCredentialsProvideris an S3A credentials provider that re-requests credentials from UCbefore they expire, keyed by table scope. It is installed on the executors too, which is why the UC
coordinates and the auth configuration travel in the Hadoop configuration: the driver's provider
instance does not cross the plan, so each side re-authenticates on its own. With
oidc, that meansthe projected token has to be mounted on the executors as well.
The cache is static, because
generateCredentialPropssetsfs.s3a.impl.disable.cache=true: Sparkrebuilds the FileSystem, and the provider with it, on every resolution, so an instance cache would
never be reused.
This is inactive while
skipCredentialVending=true, which is the current setting. It is here sothat turning vending on later is a configuration change rather than a code change.
What is deliberately not here
Upstream 0.3.x ships a
connectors/hadoopmodule for this, whoseGenericCredentialProviderisgeneric over four clouds and five credential scopes, and which rebuilds providers through
TokenProvider.configs(). Not backported:client.internal.Clock,ApiClientUtils,RetryingApiClient,client.delta.api.*,client.auth.TokenProvider);Scala 2.13 only, while this distribution is Spark 3.5.8 / Scala 2.12.
So the provider here collapses that hierarchy into one table-scoped, S3-only class. There is no
configs()accessor either: it is only needed to rebuild a provider generically, which thissingle-purpose provider does directly from the Hadoop configuration.
Compatibility
Existing modes are untouched and keep precedence: an explicit
typewins, otherwisetokeninfersstaticand theoauth.*keys inferoauth. A blank value counts as unset, so an unauthenticatedmetastore stays unauthenticated. No current catalog configuration has to change.
Tests
60 tests passing: 31 unit + the 29
TableReadWriteTestintegration tests.FileOidcUCTokenProviderTest(6): request shape, caching, re-read + re-exchange after expiry,non-200 propagation, missing and empty token file.
UCTokenProviderTest(8): dispatch per type, custom class instantiation and its failure, missingtype, incompleteoauth.*/oidc.*.AuthConfigUtilsTest(12): prefix stripping, type inference for the three legacy shapes, explicittype winning, double-configuration rejection, blank values, case insensitivity.
S3VendedCredentialsProviderTest(5): bootstrap credentials served without calling UC, absentexpiry treated as non-expiring, renewal triggered inside the lead time, cache shared across
provider instances of one scope, scopes isolated from each other.
Note: this project's
build.sbtasserts JDK 17+.Notes for the reviewer
configs()accessor that had nocaller, and commit 4 fixes a regression commit 2 introduced. The history carries the reasoning.
both are covered with mocks. The renewal path in particular is exercised only up to the point where
it would call UC.
build.sbtgainssoftware.amazon.awssdk:authasProvided, alongside the existing GCS and ABFSSPI dependencies.
hadoop-awssupplies it at runtime.oidcthroughUCSingleCatalogtoday: the LBCTRANSDA-567consumers all go through the Databricks SDK, not this connector.