Skip to content

feat(grpc): Call credentials API#2529

Merged
arjan-bal merged 13 commits intohyperium:masterfrom
arjan-bal:call-credentials
Mar 9, 2026
Merged

feat(grpc): Call credentials API#2529
arjan-bal merged 13 commits intohyperium:masterfrom
arjan-bal:call-credentials

Conversation

@arjan-bal
Copy link
Copy Markdown
Collaborator

@arjan-bal arjan-bal commented Mar 2, 2026

This change introduces a Call Credentials API for gRPC, allowing users to attach authentication metadata (such as OAuth or JWT tokens) to individual RPCs.

API Design

  • CallCredentials: The base trait that specific credential implementations must implement.
  • Ergonomics: Public-facing APIs accept Arc<dyn CallCredentials>.

Why Reference Counting?

Call credentials must be reference-counted (Arc) to support sharing across out-of-band gRPC channels, such as those used by Load Balancers for load reporting.

Functional Changes

  • Added fn get_call_credentials(&self) -> Option<&CallCredentials> to the ChannelCredentials trait. This enables attaching call credentials to channel-level credentials that apply to every RPC.
  • CompositeCallCredentials: A new utility for combining multiple call credentials.
  • CompositeChannelCredentials: A new utility for attaching call credentials to an existing channel credentials object.

Copy link
Copy Markdown
Collaborator

@dfawley dfawley left a comment

Choose a reason for hiding this comment

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

I'm not sure whether I like the impl Into<CallCredentials>. It is nice in some ways, but:

  1. It adds an extra type (CallCredentials vs. CallCredentialsProvider) to the API surface.
  2. It requires the user to know that there is a blanket impl<T: Provider> From<T> for CC.
  3. It only saves the user needing to do an Arc::new() (or .clone()) when configuring the channel.
  4. It means if the user already Arcs their Provider, e.g. if they're going to use the same call creds for multiple channels, then this arcs their arc, which is wasteful in the same ways you're trying to avoid.

&self.service_url
}

/// The method name suffix (e.g., `Method` or `package.Service/Method`).
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This example lost me. Why would the method suffix include the service and a slash? Or did you typo of->or?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The "or" should have been "in". Fixed that now.

While gRPC Go only sends the service URL, after stripping the method to the call credentials, gRPC C++ also sends the method name separately. I've gone with the C++ approach here.

use crate::attributes::Attributes;
use crate::credentials::common::SecurityLevel;

/// Details regarding the RPC call.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: "RPC call" -> "RPC" or just "call".

(x3)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed.

) -> Result<(), Status>;

/// Indicates the minimum transport security level required to send
/// these credentials.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Does the docstring need to call out the default value here?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ping - I'm not sure what the documentation norms are for this kind of situation (default impls in traits) off the top of my head.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Added a section in the rustdoc.

SecurityLevel::IntegrityOnly => 1,
SecurityLevel::PrivacyAndIntegrity => 2,
})
.unwrap_or(SecurityLevel::NoSecurity)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

PrivacyAndIntegrity?

Although, why would unwrap fail here? Only if there were zero elements in self.creds (which is impossible)?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Because the CompositeCallCredentials constructor requires two call credentials, the vector is guaranteed to be non-empty. I've replaced this with an expect() call and a clear message.

@dfawley dfawley assigned arjan-bal and unassigned dfawley Mar 3, 2026
@arjan-bal
Copy link
Copy Markdown
Collaborator Author

I'm not sure whether I like the impl Into<CallCredentials>. It is nice in some ways, but:

  1. It adds an extra type (CallCredentials vs. CallCredentialsProvider) to the API surface.
  2. It requires the user to know that there is a blanket impl<T: Provider> From<T> for CC.
  3. It only saves the user needing to do an Arc::new() (or .clone()) when configuring the channel.
  4. It means if the user already Arcs their Provider, e.g. if they're going to use the same call creds for multiple channels, then this arcs their arc, which is wasteful in the same ways you're trying to avoid.

I don't have strong opinions on this, and I see the benefit of having a simpler API surface. The double Arcing can be avoided if the user creates the wrapper object (CallCredentials) once, and passed it to the channel constructor.

I've replaced the wrapper type with Arc<dyn CallCredentials> directly.

@arjan-bal arjan-bal assigned dfawley and unassigned arjan-bal Mar 4, 2026
) -> Result<(), Status>;

/// Indicates the minimum transport security level required to send
/// these credentials.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ping - I'm not sure what the documentation norms are for this kind of situation (default impls in traits) off the top of my head.

Comment on lines 219 to 221
fn get_call_credentials(&self) -> Option<Arc<dyn CallCredentials>> {
Some(self.call_creds.clone())
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm not sure if it's worth it but this could still return Option<&Arc<dyn CallCredentials>> to delay the clone as much as possible. (That's effectively what you were doing before, too.)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done.

@dfawley dfawley assigned arjan-bal and unassigned dfawley Mar 4, 2026
Copy link
Copy Markdown
Collaborator

@dfawley dfawley left a comment

Choose a reason for hiding this comment

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

Looks like we're waiting for a new version of semver (w/ obi1kenobi/trustfall-rustdoc-adapter#1044), to work around a bug in rust (rust-lang/rust#153465), which should be available soon.

@arjan-bal arjan-bal merged commit 45257a1 into hyperium:master Mar 9, 2026
41 of 42 checks passed
@arjan-bal arjan-bal deleted the call-credentials branch March 9, 2026 09:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants