Skip to content

[SPARK-57890][CORE] Add core credential types (UserContext, ServiceCredential, UserCredentials)#57140

Closed
yadavay-amzn wants to merge 3 commits into
apache:masterfrom
yadavay-amzn:SPARK-57890
Closed

[SPARK-57890][CORE] Add core credential types (UserContext, ServiceCredential, UserCredentials)#57140
yadavay-amzn wants to merge 3 commits into
apache:masterfrom
yadavay-amzn:SPARK-57890

Conversation

@yadavay-amzn

@yadavay-amzn yadavay-amzn commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

This adds the foundational credential types for the OIDC Credential Propagation SPIP (SPARK-57703).

Three types are introduced in org.apache.spark.security (spark-core), all @DeveloperApi:

  • UserContext : driver-side OIDC identity (principal, issuer, rawToken, issuedAt, expiresAt). It is intentionally not Serializable (driver-only, never transmitted to executors). Its toString() redacts rawToken as [REDACTED], and getRawToken() is annotated @JsonIgnore so the raw token is excluded from Jackson serialization.
  • ServiceCredential : a short-lived, scoped delegated credential (properties: Map<String, String>, expiresAt). Serializable. Its toString() redacts the property values (keys shown, values [REDACTED]) since they may hold cloud secret keys.
  • UserCredentials : a per-scheme Map<String, ServiceCredential> bundle. Serializable, with a forScheme(String) lookup returning Optional<ServiceCredential>. It contains no UserContext or raw token.

The types are implemented in Java so they are directly usable from both Scala and Java and from the Java CredentialProvider SPI added in the follow-up (SPARK-57891). All are immutable.

Why are the changes needed?

There are currently no types in Spark to represent an OIDC identity or short-lived delegated credentials for propagation to executors. These are the foundational types the rest of the credential-propagation framework builds on. See the SPIP design document, Appendix A.

Does this PR introduce any user-facing change?

No behavior change. It adds new @DeveloperApi types only.

How was this patch tested?

New JUnit CredentialTypesSuite covering the acceptance criteria: rawToken redaction in toString(), exclusion of rawToken from Jackson serialization, ServiceCredential.toString() value redaction, Java-serialization round-trip for ServiceCredential/UserCredentials (no loss), forScheme present/absent lookup, and expiry checks.

Was this patch authored or co-authored using generative AI tooling?

No.

@shrirangmhalgi shrirangmhalgi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM. Reviewed the security surface and API design:

What I verified:

  • rawToken redaction: toString() prints [REDACTED], @JsonIgnore excludes from Jackson serialization
  • UserContext non-Serializable: test confirms NotSerializableException (driver-only, never transmitted to executors)
  • Defensive copies: ServiceCredential(properties) and UserCredentials(credentials) both copy input maps; getters return unmodifiableMap
  • Case-insensitive forScheme: normalized to Locale.ROOT lowercase at construction
  • Immutability: all fields final, no mutable state, thread-safe without synchronization
  • serialVersionUID on both Serializable types
  • ServiceCredential.toString() redacts property values (keys shown for debuggability, values [REDACTED])
  • UserCredentials.toString() delegates correctly - HashMap.toString() calls ServiceCredential.toString() per entry
  • Jackson dependency: jackson-databind already in spark-core pom.xml, no new dependency
  • Identity uniqueness: equals() includes both principal + issuer (sub is only unique within an issuer)

Overall clean foundation for the credential propagation framework.

@sarutak sarutak self-requested a review July 8, 2026 23:25
}

/** Returns the raw OIDC JWT. This value must never be logged or transmitted to executors. */
@JsonIgnore

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.

UserContext is not Serializable and not intended to be written as JSON. Is there any reason to annotate as @JsonIgnore?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You're right, there's no reachable JSON path, so this is just being defensive against a path that doesn't exist.
Removed both annotations.

*/
public UserCredentials(Map<String, ServiceCredential> credentials) {
Objects.requireNonNull(credentials, "credentials must not be null");
Map<String, ServiceCredential> normalized = new HashMap<>(credentials.size());

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.

The comment says "the last entry (in iteration order) wins" for case-colliding keys, but HashMap's iteration order is non-deterministic, making this behavior effectively unpredictable. For a credential system, silent non-deterministic behavior seems risky.

Since the SPIP design document only specifies "per-scheme Map[String, ServiceCredential]" without requiring last-wins semantics, I'd suggest throwing on collision instead:

String normalized = entry.getKey().toLowerCase(Locale.ROOT);
if (normalized.put(normalized, entry.getValue()) != null) {
  throw new IllegalArgumentException(
      "Duplicate scheme after case normalization: " + entry.getKey());
}

And updating the Javadoc accordingly:

- If the supplied map contains keys that differ only by case, the last entry (in iteration order) wins.
+ If the supplied map contains keys that differ only by case, an {@link IllegalArgumentException} is thrown.

This is safer and can always be relaxed to last-wins later (with a LinkedHashMap) if needed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, silent non-deterministic last-wins is not the right default for a credential type.

Changed the constructor to throw IllegalArgumentException on a post-normalization collision and updated the Javadoc to match.
Also added testUserCredentialsRejectsCaseCollidingSchemes

Objects.requireNonNull(credentials, "credentials must not be null");
Map<String, ServiceCredential> normalized = new HashMap<>(credentials.size());
for (Map.Entry<String, ServiceCredential> entry : credentials.entrySet()) {
normalized.put(entry.getKey().toLowerCase(Locale.ROOT), entry.getValue());

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.

HashMap permits null keys, so if a caller passes a map containing a null key, .toLowerCase() will throw an NPE without a clear message. A brief null check would make the failure more informative:

Objects.requireNonNull(entry.getKey(), "scheme key must not be null");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Instant expiresAt) {
this.principal = Objects.requireNonNull(principal, "principal must not be null");
this.issuer = Objects.requireNonNull(issuer, "issuer must not be null");
this.rawToken = Objects.requireNonNull(rawToken, "rawToken must not be null");

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.

I'd suggest excluding rawToken from equals() and hashCode(). My reasoning:

  • UserContext is driver-only, not used as a map key, and there's no use case for equality comparison beyond testing. Including secret material in equals/hashCode when there's no practical need for it seems unnecessary.
  • If exact token comparison becomes necessary in the future, a dedicated method (e.g., isIdenticalToken(UserContext)) can be added without breaking existing equality semantics.
  • For testing, rawToken can be verified separately via assertEquals(expected, ctx.getRawToken()).

What do you think?
If exact token comparison is needed in the future, a dedicated method (e.g., isIdenticalToken(UserContext)) can be added without breaking existing equality semantics.

@yadavay-amzn yadavay-amzn Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Makes sense - I was considering to remove it while implementing as well.

Excluded rawToken from both equals() and hashCode(). The identity fields (principal, issuer) plus the validity window now define equality.
I also updated the test to assert two contexts differing only by rawToken compare equal.
Happy to add isIdenticalToken(...) later if an exact-token comparison is ever needed.

…g/null scheme keys, exclude rawToken from equals/hashCode
@sarutak

sarutak commented Jul 9, 2026

Copy link
Copy Markdown
Member

The description says as follows.

This adds the foundational credential types for the OIDC Credential Propagation SPIP (SPARK-57703), task 1 of 3.

It's unclear what "3" refers to here (3 deliverable units in the SPIP? 12 sub-tasks in JIRA?). I'd suggest just dropping it like:

- This adds the foundational credential types for the OIDC Credential Propagation SPIP (SPARK-57703), task 1 of 3.
+ This adds the foundational credential types for the OIDC Credential Propagation SPIP (SPARK-57703).

@yadavay-amzn

Copy link
Copy Markdown
Contributor Author

I was referring to this PR as the first of the 3 tasks of the SPIP which together provide the foundational credential types and interfaces.
However, I agree that the wording is a bit confusing and ambiguous, updated the PR description to remove that part.

Also re-running the CI jobs, since the failures seem unrelated to the PR changes.

@sarutak

sarutak commented Jul 9, 2026

Copy link
Copy Markdown
Member

I believe the CI failure is not relevant this change so I'll merge this.

@sarutak sarutak closed this in 99ad64c Jul 9, 2026
sarutak pushed a commit that referenced this pull request Jul 9, 2026
…edential, UserCredentials)

### What changes were proposed in this pull request?

This adds the foundational credential types for the OIDC Credential Propagation SPIP ([SPARK-57703](https://issues.apache.org/jira/browse/SPARK-57703)).

Three types are introduced in `org.apache.spark.security` (spark-core), all `DeveloperApi`:

- `UserContext` : driver-side OIDC identity (`principal`, `issuer`, `rawToken`, `issuedAt`, `expiresAt`). It is intentionally **not** `Serializable` (driver-only, never transmitted to executors). Its `toString()` redacts `rawToken` as `[REDACTED]`, and `getRawToken()` is annotated `JsonIgnore` so the raw token is excluded from Jackson serialization.
- `ServiceCredential` : a short-lived, scoped delegated credential (`properties: Map<String, String>`, `expiresAt`). `Serializable`. Its `toString()` redacts the property values (keys shown, values `[REDACTED]`) since they may hold cloud secret keys.
- `UserCredentials` : a per-scheme `Map<String, ServiceCredential>` bundle. `Serializable`, with a `forScheme(String)` lookup returning `Optional<ServiceCredential>`. It contains no `UserContext` or raw token.

The types are implemented in Java so they are directly usable from both Scala and Java and from the Java `CredentialProvider` SPI added in the follow-up (SPARK-57891). All are immutable.

### Why are the changes needed?

There are currently no types in Spark to represent an OIDC identity or short-lived delegated credentials for propagation to executors. These are the foundational types the rest of the credential-propagation framework builds on. See the SPIP design document, Appendix A.

### Does this PR introduce _any_ user-facing change?

No behavior change. It adds new `DeveloperApi` types only.

### How was this patch tested?

New JUnit `CredentialTypesSuite` covering the acceptance criteria: `rawToken` redaction in `toString()`, exclusion of `rawToken` from Jackson serialization, `ServiceCredential.toString()` value redaction, Java-serialization round-trip for `ServiceCredential`/`UserCredentials` (no loss), `forScheme` present/absent lookup, and expiry checks.

### Was this patch authored or co-authored using generative AI tooling?

No.

Closes #57140 from yadavay-amzn/SPARK-57890.

Authored-by: Anupam Yadav <anupamy030@gmail.com>
Signed-off-by: Kousuke Saruta <sarutak@apache.org>
(cherry picked from commit 99ad64c)
Signed-off-by: Kousuke Saruta <sarutak@apache.org>
@sarutak

sarutak commented Jul 9, 2026

Copy link
Copy Markdown
Member

Merge Summary:

Posted by merge_spark_pr.py

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.

3 participants