Skip to content

Commit

Permalink
Implement trusted publishing (#7548)
Browse files Browse the repository at this point in the history
Co-authored-by: Charlie Marsh <[email protected]>
  • Loading branch information
konstin and charliermarsh authored Sep 24, 2024
1 parent c053dc8 commit 205bf8c
Show file tree
Hide file tree
Showing 22 changed files with 501 additions and 39 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,9 @@ jobs:
env:
# No dbus in GitHub Actions
PYTHON_KEYRING_BACKEND: keyrings.alt.file.PlaintextKeyring
permissions:
# For trusted publishing
id-token: write
steps:
- uses: actions/checkout@v4
with:
Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use url::Url;
use uv_cache::CacheArgs;
use uv_configuration::{
ConfigSettingEntry, ExportFormat, IndexStrategy, KeyringProviderType, PackageNameSpecifier,
TargetTriple, TrustedHost,
TargetTriple, TrustedHost, TrustedPublishing,
};
use uv_normalize::{ExtraName, PackageName};
use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
Expand Down Expand Up @@ -4347,6 +4347,14 @@ pub struct PublishArgs {
)]
pub token: Option<String>,

/// Configure using trusted publishing through GitHub Actions.
///
/// By default, uv checks for trusted publishing when running in GitHub Actions, but ignores it
/// if it isn't configured or the workflow doesn't have enough permissions (e.g., a pull request
/// from a fork).
#[arg(long)]
pub trusted_publishing: Option<TrustedPublishing>,

/// Attempt to use `keyring` for authentication for remote requirements files.
///
/// At present, only `--keyring-provider subprocess` is supported, which configures uv to
Expand Down
78 changes: 65 additions & 13 deletions crates/uv-client/src/base_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@ use crate::middleware::OfflineMiddleware;
use crate::tls::read_identity;
use crate::Connectivity;

/// Selectively skip parts or the entire auth middleware.
#[derive(Debug, Clone, Copy, Default)]
pub enum AuthIntegration {
/// Run the full auth middleware, including sending an unauthenticated request first.
#[default]
Default,
/// Send only an authenticated request without cloning and sending an unauthenticated request
/// first. Errors if no credentials were found.
OnlyAuthenticated,
/// Skip the auth middleware entirely. The caller is responsible for managing authentication.
NoAuthMiddleware,
}

/// A builder for an [`BaseClient`].
#[derive(Debug, Clone)]
pub struct BaseClientBuilder<'a> {
Expand All @@ -36,7 +49,7 @@ pub struct BaseClientBuilder<'a> {
client: Option<Client>,
markers: Option<&'a MarkerEnvironment>,
platform: Option<&'a Platform>,
only_authenticated: bool,
auth_integration: AuthIntegration,
}

impl Default for BaseClientBuilder<'_> {
Expand All @@ -56,7 +69,7 @@ impl BaseClientBuilder<'_> {
client: None,
markers: None,
platform: None,
only_authenticated: false,
auth_integration: AuthIntegration::default(),
}
}
}
Expand Down Expand Up @@ -111,8 +124,8 @@ impl<'a> BaseClientBuilder<'a> {
}

#[must_use]
pub fn only_authenticated(mut self, only_authenticated: bool) -> Self {
self.only_authenticated = only_authenticated;
pub fn auth_integration(mut self, auth_integration: AuthIntegration) -> Self {
self.auth_integration = auth_integration;
self
}

Expand Down Expand Up @@ -162,34 +175,53 @@ impl<'a> BaseClientBuilder<'a> {
debug!("Using request timeout of {timeout}s");

// Create a secure client that validates certificates.
let client = self.create_client(
let raw_client = self.create_client(
&user_agent_string,
timeout,
ssl_cert_file_exists,
Security::Secure,
);

// Create an insecure client that accepts invalid certificates.
let dangerous_client = self.create_client(
let raw_dangerous_client = self.create_client(
&user_agent_string,
timeout,
ssl_cert_file_exists,
Security::Insecure,
);

// Wrap in any relevant middleware and handle connectivity.
let client = self.apply_middleware(client);
let dangerous_client = self.apply_middleware(dangerous_client);
let client = self.apply_middleware(raw_client.clone());
let dangerous_client = self.apply_middleware(raw_dangerous_client.clone());

BaseClient {
connectivity: self.connectivity,
allow_insecure_host: self.allow_insecure_host.clone(),
client,
raw_client,
dangerous_client,
raw_dangerous_client,
timeout,
}
}

/// Share the underlying client between two different middleware configurations.
pub fn wrap_existing(&self, existing: &BaseClient) -> BaseClient {
// Wrap in any relevant middleware and handle connectivity.
let client = self.apply_middleware(existing.raw_client.clone());
let dangerous_client = self.apply_middleware(existing.raw_dangerous_client.clone());

BaseClient {
connectivity: self.connectivity,
allow_insecure_host: self.allow_insecure_host.clone(),
client,
dangerous_client,
raw_client: existing.raw_client.clone(),
raw_dangerous_client: existing.raw_dangerous_client.clone(),
timeout: existing.timeout,
}
}

fn create_client(
&self,
user_agent: &str,
Expand Down Expand Up @@ -253,11 +285,22 @@ impl<'a> BaseClientBuilder<'a> {
}

// Initialize the authentication middleware to set headers.
client = client.with(
AuthMiddleware::new()
.with_keyring(self.keyring.to_provider())
.with_only_authenticated(self.only_authenticated),
);
match self.auth_integration {
AuthIntegration::Default => {
client = client
.with(AuthMiddleware::new().with_keyring(self.keyring.to_provider()));
}
AuthIntegration::OnlyAuthenticated => {
client = client.with(
AuthMiddleware::new()
.with_keyring(self.keyring.to_provider())
.with_only_authenticated(true),
);
}
AuthIntegration::NoAuthMiddleware => {
// The downstream code uses custom auth logic.
}
}

client.build()
}
Expand All @@ -275,6 +318,10 @@ pub struct BaseClient {
client: ClientWithMiddleware,
/// The underlying HTTP client that accepts invalid certificates.
dangerous_client: ClientWithMiddleware,
/// The HTTP client without middleware.
raw_client: Client,
/// The HTTP client that accepts invalid certificates without middleware.
raw_dangerous_client: Client,
/// The connectivity mode to use.
connectivity: Connectivity,
/// Configured client timeout, in seconds.
Expand All @@ -297,6 +344,11 @@ impl BaseClient {
self.client.clone()
}

/// The underlying [`Client`] without middleware.
pub fn raw_client(&self) -> Client {
self.raw_client.clone()
}

/// Selects the appropriate client based on the host's trustworthiness.
pub fn for_host(&self, url: &Url) -> &ClientWithMiddleware {
if self
Expand Down
2 changes: 1 addition & 1 deletion crates/uv-client/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pub use base_client::{BaseClient, BaseClientBuilder};
pub use base_client::{AuthIntegration, BaseClient, BaseClientBuilder};
pub use cached_client::{CacheControl, CachedClient, CachedClientError, DataWithCachePolicy};
pub use error::{Error, ErrorKind, WrappedReqwestError};
pub use flat_index::{FlatIndexClient, FlatIndexEntries, FlatIndexError};
Expand Down
2 changes: 2 additions & 0 deletions crates/uv-configuration/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub use preview::*;
pub use sources::*;
pub use target_triple::*;
pub use trusted_host::*;
pub use trusted_publishing::*;

mod authentication;
mod build_options;
Expand All @@ -35,3 +36,4 @@ mod preview;
mod sources;
mod target_triple;
mod trusted_host;
mod trusted_publishing;
15 changes: 15 additions & 0 deletions crates/uv-configuration/src/trusted_publishing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use serde::{Deserialize, Serialize};

#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum TrustedPublishing {
/// Try trusted publishing when we're already in GitHub Actions, continue if that fails.
#[default]
Automatic,
// Force trusted publishing.
Always,
// Never try to get a trusted publishing token.
Never,
}
2 changes: 2 additions & 0 deletions crates/uv-publish/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ license.workspace = true
distribution-filename = { workspace = true }
pypi-types = { workspace = true }
uv-client = { workspace = true }
uv-configuration = { workspace = true }
uv-fs = { workspace = true }
uv-metadata = { workspace = true }
uv-warnings = { workspace = true }

async-compression = { workspace = true }
base64 = { workspace = true }
Expand Down
Loading

0 comments on commit 205bf8c

Please sign in to comment.