Skip to content

Commit

Permalink
Add trusted publisher support
Browse files Browse the repository at this point in the history
  • Loading branch information
messense committed Apr 26, 2023
1 parent df65f10 commit 17aef58
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 3 deletions.
3 changes: 3 additions & 0 deletions Cargo.lock

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

5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"]

Expand Down
77 changes: 76 additions & 1 deletion src/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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))
{
Expand All @@ -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<Option<String>> {
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(&req_url)
.timeout(Duration::from_secs(30))
.call()?
.into_json()?;
let audience = audience_res.audience;

debug!("Requesting OIDC token from {}", 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::<MintTokenResponse>()?;
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<Registry> {
// load creds from pypirc if found
Expand Down Expand Up @@ -248,7 +323,7 @@ fn complete_registry(opt: &PublishOpt) -> Result<Registry> {
opt.repository
);
};
let (username, password) = resolve_pypi_cred(opt, &pypirc, registry_name);
let (username, password) = resolve_pypi_cred(opt, &pypirc, registry_name, &registry_url);
let registry = Registry::new(username, password, registry_url);

Ok(registry)
Expand Down

0 comments on commit 17aef58

Please sign in to comment.