-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Support Gitlab CI/CD as a trusted publisher #15583
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
Merged
Merged
Changes from 16 commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
2fed864
Enhance trusted publishing support for GitLab CI
harsh-ps-2003 de660d5
test nit
harsh-ps-2003 7fc20b8
Refactor GitLab trusted publishing tests and update OIDC token handling
harsh-ps-2003 71c6e3e
Add audience endpoint mocks for PyPI and TestPyPI in GitLab trusted p…
harsh-ps-2003 50864a3
fmt nit
harsh-ps-2003 2e990d5
- Updated the `get_token` function to use a normalized environment ke…
harsh-ps-2003 612bed1
error nit
harsh-ps-2003 ad50d9e
Prefer HTTPS for OIDC minting; allow HTTP only in test builds
harsh-ps-2003 9ff70c1
test refactor
harsh-ps-2003 527108d
nit
harsh-ps-2003 655c7b7
switch to ambient_id for OIDC token discovery
woodruffw 699b54d
Merge branch 'main' into gitlab
woodruffw cc12bcf
fix clippy findings
woodruffw be876cd
fix some tests
woodruffw 170fe8e
fix remaining tests
woodruffw 167062d
bump docs, run full clippy
woodruffw 1c95e95
bump ambient_id
woodruffw 4ee48ae
specialize GitHub permissions error case rendering
woodruffw ea4d25d
push IdToken further into params
woodruffw 2a33439
improve suggestion on NoToken
woodruffw d272484
bump snapshot
woodruffw c800f63
Merge remote-tracking branch 'origin/main' into gitlab
woodruffw f1dc54d
docs: update trusted publishing docs to include GitLab CI/CD
woodruffw File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,11 @@ | ||
//! Trusted publishing (via OIDC) with GitHub actions. | ||
//! Trusted publishing (via OIDC) with GitHub Actions and GitLab CI. | ||
|
||
use base64::Engine; | ||
use base64::prelude::BASE64_URL_SAFE_NO_PAD; | ||
use reqwest::{StatusCode, header}; | ||
use reqwest::StatusCode; | ||
use reqwest_middleware::ClientWithMiddleware; | ||
use serde::{Deserialize, Serialize}; | ||
use std::env; | ||
use std::env::VarError; | ||
use std::ffi::OsString; | ||
use std::fmt::Display; | ||
use thiserror::Error; | ||
use tracing::{debug, trace}; | ||
|
@@ -17,12 +15,10 @@ use uv_static::EnvVars; | |
|
||
#[derive(Debug, Error)] | ||
pub enum TrustedPublishingError { | ||
#[error("Environment variable {0} not set, is the `id-token: write` permission missing?")] | ||
MissingEnvVar(&'static str), | ||
#[error("Environment variable {0} is not valid UTF-8: `{1:?}`")] | ||
InvalidEnvVar(&'static str, OsString), | ||
#[error(transparent)] | ||
Url(#[from] url::ParseError), | ||
#[error("Failed to discover OIDC token")] | ||
Discovery(#[from] ambient_id::Error), | ||
#[error("Failed to fetch: `{0}`")] | ||
Reqwest(DisplaySafeUrl, #[source] reqwest::Error), | ||
#[error("Failed to fetch: `{0}`")] | ||
|
@@ -36,15 +32,10 @@ pub enum TrustedPublishingError { | |
/// When trusted publishing is misconfigured, the error above should occur, not this one. | ||
#[error("PyPI returned error code {0}, and the OIDC has an unexpected format.\nResponse: {1}")] | ||
InvalidOidcToken(StatusCode, String), | ||
} | ||
|
||
impl TrustedPublishingError { | ||
fn from_var_err(env_var: &'static str, err: VarError) -> Self { | ||
match err { | ||
VarError::NotPresent => Self::MissingEnvVar(env_var), | ||
VarError::NotUnicode(os_string) => Self::InvalidEnvVar(env_var, os_string), | ||
} | ||
} | ||
/// This error can only occur when the user forces the use of trusted publishing | ||
/// in an unsupported environment. | ||
#[error("No OIDC token available from the environment")] | ||
woodruffw marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
NoToken, | ||
} | ||
|
||
#[derive(Deserialize)] | ||
|
@@ -63,12 +54,6 @@ struct Audience { | |
audience: String, | ||
} | ||
|
||
/// The response from querying `$ACTIONS_ID_TOKEN_REQUEST_URL&audience=pypi`. | ||
#[derive(Deserialize)] | ||
struct OidcToken { | ||
value: String, | ||
} | ||
|
||
/// The body for querying `$ACTIONS_ID_TOKEN_REQUEST_URL&audience=pypi`. | ||
#[derive(Serialize)] | ||
struct MintTokenRequest { | ||
|
@@ -94,34 +79,39 @@ pub struct OidcTokenClaims { | |
} | ||
|
||
/// Returns the short-lived token to use for uploading. | ||
/// | ||
/// Return states: | ||
/// - `Ok(Some(token))`: Successfully obtained a trusted publishing token. | ||
/// - `Ok(None)`: Not in a supported CI environment for trusted publishing. | ||
/// - `Err(...)`: An error occurred while trying to obtain the token. | ||
pub(crate) async fn get_token( | ||
registry: &DisplaySafeUrl, | ||
client: &ClientWithMiddleware, | ||
) -> Result<TrustedPublishingToken, TrustedPublishingError> { | ||
// If this fails, we can skip the audience request. | ||
let oidc_token_request_token = | ||
env::var(EnvVars::ACTIONS_ID_TOKEN_REQUEST_TOKEN).map_err(|err| { | ||
TrustedPublishingError::from_var_err(EnvVars::ACTIONS_ID_TOKEN_REQUEST_TOKEN, err) | ||
})?; | ||
|
||
// Request 1: Get the audience | ||
) -> Result<Option<TrustedPublishingToken>, TrustedPublishingError> { | ||
// Get the OIDC token's audience from the registry. | ||
let audience = get_audience(registry, client).await?; | ||
|
||
// Request 2: Get the OIDC token from GitHub. | ||
let oidc_token = get_oidc_token(&audience, &oidc_token_request_token, client).await?; | ||
// Perform ambient OIDC token discovery. | ||
// Depending on the host (GitHub Actions, GitLab CI, etc.) | ||
// this may perform additional network requests. | ||
let oidc_token = get_oidc_token(&audience, client).await?; | ||
|
||
// Request 3: Get the publishing token from PyPI. | ||
let publish_token = get_publish_token(registry, &oidc_token, client).await?; | ||
// Exchange the OIDC token for a short-lived upload token, | ||
// if OIDC token discovery succeeded. | ||
if let Some(oidc_token) = &oidc_token { | ||
let publish_token = get_publish_token(registry, oidc_token.reveal(), client).await?; | ||
woodruffw marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
debug!("Received token, using trusted publishing"); | ||
// If we're on GitHub Actions, mask the exchanged token in logs. | ||
#[allow(clippy::print_stdout)] | ||
if env::var(EnvVars::GITHUB_ACTIONS) == Ok("true".to_string()) { | ||
println!("::add-mask::{publish_token}"); | ||
} | ||
|
||
// Tell GitHub Actions to mask the token in any console logs. | ||
#[allow(clippy::print_stdout)] | ||
if env::var(EnvVars::GITHUB_ACTIONS) == Ok("true".to_string()) { | ||
println!("::add-mask::{}", &publish_token); | ||
Ok(Some(publish_token)) | ||
} else { | ||
// Not in a supported CI environment for trusted publishing. | ||
Ok(None) | ||
} | ||
|
||
Ok(publish_token) | ||
} | ||
|
||
async fn get_audience( | ||
|
@@ -130,8 +120,17 @@ async fn get_audience( | |
) -> Result<String, TrustedPublishingError> { | ||
// `pypa/gh-action-pypi-publish` uses `netloc` (RFC 1808), which is deprecated for authority | ||
// (RFC 3986). | ||
let audience_url = | ||
DisplaySafeUrl::parse(&format!("https://{}/_/oidc/audience", registry.authority()))?; | ||
// Prefer HTTPS for OIDC discovery; allow HTTP only in test builds | ||
let scheme: &str = if cfg!(feature = "test") { | ||
registry.scheme() | ||
} else { | ||
"https" | ||
}; | ||
konstin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let audience_url = DisplaySafeUrl::parse(&format!( | ||
"{}://{}/_/oidc/audience", | ||
scheme, | ||
registry.authority() | ||
))?; | ||
debug!("Querying the trusted publishing audience from {audience_url}"); | ||
let response = client | ||
.get(Url::from(audience_url.clone())) | ||
|
@@ -148,33 +147,14 @@ async fn get_audience( | |
Ok(audience.audience) | ||
} | ||
|
||
/// Perform ambient OIDC token discovery. | ||
async fn get_oidc_token( | ||
audience: &str, | ||
oidc_token_request_token: &str, | ||
client: &ClientWithMiddleware, | ||
) -> Result<String, TrustedPublishingError> { | ||
let oidc_token_url = env::var(EnvVars::ACTIONS_ID_TOKEN_REQUEST_URL).map_err(|err| { | ||
TrustedPublishingError::from_var_err(EnvVars::ACTIONS_ID_TOKEN_REQUEST_URL, err) | ||
})?; | ||
let mut oidc_token_url = DisplaySafeUrl::parse(&oidc_token_url)?; | ||
oidc_token_url | ||
.query_pairs_mut() | ||
.append_pair("audience", audience); | ||
debug!("Querying the trusted publishing OIDC token from {oidc_token_url}"); | ||
let authorization = format!("bearer {oidc_token_request_token}"); | ||
let response = client | ||
.get(Url::from(oidc_token_url.clone())) | ||
.header(header::AUTHORIZATION, authorization) | ||
.send() | ||
.await | ||
.map_err(|err| TrustedPublishingError::ReqwestMiddleware(oidc_token_url.clone(), err))?; | ||
let oidc_token: OidcToken = response | ||
.error_for_status() | ||
.map_err(|err| TrustedPublishingError::Reqwest(oidc_token_url.clone(), err))? | ||
.json() | ||
.await | ||
.map_err(|err| TrustedPublishingError::Reqwest(oidc_token_url.clone(), err))?; | ||
Ok(oidc_token.value) | ||
) -> Result<Option<ambient_id::IdToken>, TrustedPublishingError> { | ||
let detector = ambient_id::Detector::new_with_client(client.clone()); | ||
|
||
Ok(detector.detect(audience).await?) | ||
|
||
} | ||
|
||
/// Parse the JSON Web Token that the OIDC token is. | ||
|
@@ -194,8 +174,15 @@ async fn get_publish_token( | |
oidc_token: &str, | ||
client: &ClientWithMiddleware, | ||
) -> Result<TrustedPublishingToken, TrustedPublishingError> { | ||
// Prefer HTTPS for OIDC minting; allow HTTP only in test builds | ||
let scheme: &str = if cfg!(feature = "test") { | ||
registry.scheme() | ||
} else { | ||
"https" | ||
}; | ||
let mint_token_url = DisplaySafeUrl::parse(&format!( | ||
"https://{}/_/oidc/mint-token", | ||
"{}://{}/_/oidc/mint-token", | ||
scheme, | ||
registry.authority() | ||
))?; | ||
debug!("Querying the trusted publishing upload token from {mint_token_url}"); | ||
|
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.