diff --git a/Cargo.lock b/Cargo.lock index 155ebe4b6..bff3fbeea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1079,6 +1079,7 @@ dependencies = [ "tracing-subscriber", "trycmd", "ureq", + "url", "which", "wild", "zip", @@ -2318,6 +2319,8 @@ dependencies = [ "native-tls", "once_cell", "rustls", + "serde", + "serde_json", "socks", "url", "webpki", diff --git a/Cargo.toml b/Cargo.toml index 1205b1b2d..80190fac8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,12 +83,13 @@ minijinja = { version = "0.31.0", optional = true } bytesize = { version = "1.0.1", optional = true } configparser = { version = "3.0.0", optional = true } multipart = { version = "0.18.0", features = ["client"], default-features = false, optional = true } -ureq = { version = "2.6.1", features = ["gzip", "socks-proxy"], default-features = false, optional = true } +ureq = { version = "2.6.1", features = ["gzip", "json", "socks-proxy"], default-features = false, optional = true } native-tls = { version = "0.2.8", optional = true } rustls = { version = "0.20.8", optional = true } rustls-pemfile = { version = "1.0.1", optional = true } keyring = { version = "2.0.0", default-features = false, features = ["linux-no-secret-service"], optional = true } wild = { version = "2.1.0", optional = true } +url = { version = "2.3.1", optional = true } [dev-dependencies] indoc = "2.0.0" @@ -107,7 +108,7 @@ log = ["tracing-subscriber"] cli-completion = ["dep:clap_complete_command"] -upload = ["ureq", "multipart", "configparser", "bytesize", "dialoguer/password", "wild"] +upload = ["ureq", "multipart", "configparser", "bytesize", "dialoguer/password", "url", "wild"] # keyring doesn't support *BSD so it's not enabled in `full` by default password-storage = ["upload", "keyring"] diff --git a/Changelog.md b/Changelog.md index 126dba87e..beecfdfb8 100644 --- a/Changelog.md +++ b/Changelog.md @@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Bump MSRV to 1.64.0 in [#1528](https://github.com/PyO3/maturin/pull/1528) * Add wildcards support to publish/upload commands on Windows in [#1534](https://github.com/PyO3/maturin/pull/1534) * Add support for configuring macOS deployment target version in `pyproject.toml` in [#1536](https://github.com/PyO3/maturin/pull/1536) +* Rewrite platform specific dependencies in `Cargo.toml` by viccie30 in [#1572](https://github.com/PyO3/maturin/pull/1572) +* Add trusted publisher support in [#1578](https://github.com/PyO3/maturin/pull/1578) ## [0.14.17] - 2023-04-06 diff --git a/src/upload.rs b/src/upload.rs index 34a52f875..32af76b4e 100644 --- a/src/upload.rs +++ b/src/upload.rs @@ -9,11 +9,14 @@ use fs_err as fs; use fs_err::File; use multipart::client::lazy::Multipart; use regex::Regex; +use serde::Deserialize; +use std::collections::HashMap; use std::env; #[cfg(any(feature = "native-tls", feature = "rustls"))] use std::ffi::OsString; use std::io; use std::path::{Path, PathBuf}; +use std::time::Duration; use thiserror::Error; use tracing::debug; @@ -196,12 +199,23 @@ fn resolve_pypi_cred( opt: &PublishOpt, config: &Ini, registry_name: Option<&str>, + registry_url: &str, ) -> (String, String) { // API token from environment variable takes priority if let Ok(token) = env::var("MATURIN_PYPI_TOKEN") { return ("__token__".to_string(), token); } + // Try to get a token via OIDC exchange + match resolve_pypi_token_via_oidc(registry_url) { + Ok(Some(token)) => { + eprintln!("🔐 Using trusted publisher for upload"); + return ("__token__".to_string(), token); + } + Ok(None) => {} + Err(e) => eprintln!("⚠️ Warning: Failed to resolve PyPI token via OIDC: {}", e), + } + if let Some((username, password)) = registry_name.and_then(|name| load_pypi_cred_from_config(config, name)) { @@ -219,6 +233,67 @@ fn resolve_pypi_cred( (username, password) } +#[derive(Debug, Deserialize)] +struct OidcAudienceResponse { + audience: String, +} + +#[derive(Debug, Deserialize)] +struct OidcTokenResponse { + value: String, +} + +#[derive(Debug, Deserialize)] +struct MintTokenResponse { + token: String, +} + +/// Trusted Publisher support for GitHub Actions +fn resolve_pypi_token_via_oidc(registry_url: &str) -> Result> { + if env::var_os("GITHUB_ACTIONS").is_none() { + return Ok(None); + } + if let (Ok(req_token), Ok(req_url)) = ( + env::var("ACTIONS_ID_TOKEN_REQUEST_TOKEN"), + env::var("ACTIONS_ID_TOKEN_REQUEST_URL"), + ) { + let registry_url = url::Url::parse(registry_url)?; + let mut audience_url = registry_url.clone(); + audience_url.set_path("_/oidc/audience"); + debug!("Requesting OIDC audience from {}", audience_url); + let agent = http_agent()?; + let audience_res: OidcAudienceResponse = agent + .get(audience_url.as_str()) + .timeout(Duration::from_secs(30)) + .call()? + .into_json()?; + let audience = audience_res.audience; + + debug!("Requesting OIDC token for {} from {}", audience, req_url); + let request_token_res: OidcTokenResponse = agent + .get(&req_url) + .query("audience", &audience) + .set("Authorization", &format!("bearer {req_token}")) + .timeout(Duration::from_secs(30)) + .call()? + .into_json()?; + let oidc_token = request_token_res.value; + + let mut mint_token_url = registry_url; + mint_token_url.set_path("_/oidc/github/mint-token"); + debug!("Requesting API token from {}", mint_token_url); + let mut mint_token_req = HashMap::new(); + mint_token_req.insert("token", oidc_token); + let mint_token_res = agent + .post(mint_token_url.as_str()) + .timeout(Duration::from_secs(30)) + .send_json(mint_token_req)? + .into_json::()?; + return Ok(Some(mint_token_res.token)); + } + Ok(None) +} + /// Asks for username and password for a registry account where missing. fn complete_registry(opt: &PublishOpt) -> Result { // load creds from pypirc if found @@ -248,7 +323,7 @@ fn complete_registry(opt: &PublishOpt) -> Result { opt.repository ); }; - let (username, password) = resolve_pypi_cred(opt, &pypirc, registry_name); + let (username, password) = resolve_pypi_cred(opt, &pypirc, registry_name, ®istry_url); let registry = Registry::new(username, password, registry_url); Ok(registry)