Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add trusted publisher support #1578

Merged
merged 1 commit into from
Apr 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
2 changes: 2 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

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) {
Copy link
Member

Choose a reason for hiding this comment

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

what happens if someone uses this with a registry that isn't pypi?

Copy link
Member Author

Choose a reason for hiding this comment

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

It will print a failure warning, but we can certainly do better to not show a warning. I'll improve it later.

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

thank you!

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(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::<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