11mod trusted_publishing;
22
3- use crate :: trusted_publishing:: TrustedPublishingError ;
4- use base64:: prelude:: BASE64_STANDARD ;
5- use base64:: Engine ;
3+ use std:: path:: { Path , PathBuf } ;
4+ use std:: sync:: Arc ;
5+ use std:: time:: { Duration , SystemTime } ;
6+ use std:: { env, fmt, io} ;
7+
68use fs_err:: tokio:: File ;
79use futures:: TryStreamExt ;
810use glob:: { glob, GlobError , PatternError } ;
@@ -15,17 +17,15 @@ use reqwest_retry::policies::ExponentialBackoff;
1517use reqwest_retry:: { RetryPolicy , Retryable , RetryableStrategy } ;
1618use rustc_hash:: FxHashSet ;
1719use serde:: Deserialize ;
18- use std:: path:: { Path , PathBuf } ;
19- use std:: sync:: Arc ;
20- use std:: time:: { Duration , SystemTime } ;
21- use std:: { env, fmt, io} ;
2220use thiserror:: Error ;
2321use tokio:: io:: { AsyncReadExt , BufReader } ;
2422use tokio:: sync:: Semaphore ;
2523use tokio_util:: io:: ReaderStream ;
2624use tracing:: { debug, enabled, trace, warn, Level } ;
2725use trusted_publishing:: TrustedPublishingToken ;
2826use url:: Url ;
27+
28+ use uv_auth:: Credentials ;
2929use uv_cache:: { Cache , Refresh } ;
3030use uv_client:: {
3131 BaseClient , MetadataFormat , OwnedArchive , RegistryClientBuilder , UvRetryableStrategy ,
@@ -41,6 +41,8 @@ use uv_pypi_types::{HashAlgorithm, HashDigest, Metadata23, MetadataError};
4141use uv_static:: EnvVars ;
4242use uv_warnings:: { warn_user, warn_user_once} ;
4343
44+ use crate :: trusted_publishing:: TrustedPublishingError ;
45+
4446#[ derive( Error , Debug ) ]
4547pub enum PublishError {
4648 #[ error( "The publish path is not a valid glob pattern: `{0}`" ) ]
@@ -370,8 +372,7 @@ pub async fn upload(
370372 filename : & DistFilename ,
371373 registry : & Url ,
372374 client : & BaseClient ,
373- username : Option < & str > ,
374- password : Option < & str > ,
375+ credentials : & Credentials ,
375376 check_url_client : Option < & CheckUrlClient < ' _ > > ,
376377 download_concurrency : & Semaphore ,
377378 reporter : Arc < impl Reporter > ,
@@ -391,8 +392,7 @@ pub async fn upload(
391392 filename,
392393 registry,
393394 client,
394- username,
395- password,
395+ credentials,
396396 & form_metadata,
397397 reporter. clone ( ) ,
398398 )
@@ -744,8 +744,7 @@ async fn build_request(
744744 filename : & DistFilename ,
745745 registry : & Url ,
746746 client : & BaseClient ,
747- username : Option < & str > ,
748- password : Option < & str > ,
747+ credentials : & Credentials ,
749748 form_metadata : & [ ( & ' static str , String ) ] ,
750749 reporter : Arc < impl Reporter > ,
751750) -> Result < ( RequestBuilder , usize ) , PublishPrepareError > {
@@ -767,17 +766,15 @@ async fn build_request(
767766 let part = Part :: stream_with_length ( file_reader, file_size) . file_name ( raw_filename. to_string ( ) ) ;
768767 form = form. part ( "content" , part) ;
769768
770- let url = if let Some ( username) = username {
771- if password. is_none ( ) {
772- // Attach the username to the URL so the authentication middleware can find the matching
773- // password.
774- let mut url = registry. clone ( ) ;
775- let _ = url. set_username ( username) ;
776- url
777- } else {
778- // We set the authorization header below.
779- registry. clone ( )
780- }
769+ // If we have a username but no password, attach the username to the URL so the authentication
770+ // middleware can find the matching password.
771+ let url = if let Some ( username) = credentials
772+ . username ( )
773+ . filter ( |_| credentials. password ( ) . is_none ( ) )
774+ {
775+ let mut url = registry. clone ( ) ;
776+ let _ = url. set_username ( username) ;
777+ url
781778 } else {
782779 registry. clone ( )
783780 } ;
@@ -793,11 +790,20 @@ async fn build_request(
793790 reqwest:: header:: ACCEPT ,
794791 "application/json;q=0.9, text/plain;q=0.8, text/html;q=0.7" ,
795792 ) ;
796- if let ( Some ( username) , Some ( password) ) = ( username, password) {
797- debug ! ( "Using username/password basic auth" ) ;
798- let credentials = BASE64_STANDARD . encode ( format ! ( "{username}:{password}" ) ) ;
799- request = request. header ( AUTHORIZATION , format ! ( "Basic {credentials}" ) ) ;
793+
794+ match credentials {
795+ Credentials :: Basic { password, .. } => {
796+ if password. is_some ( ) {
797+ debug ! ( "Using HTTP Basic authentication" ) ;
798+ request = request. header ( AUTHORIZATION , credentials. to_header_value ( ) ) ;
799+ }
800+ }
801+ Credentials :: Bearer { .. } => {
802+ debug ! ( "Using Bearer token authentication" ) ;
803+ request = request. header ( AUTHORIZATION , credentials. to_header_value ( ) ) ;
804+ }
800805 }
806+
801807 Ok ( ( request, idx) )
802808}
803809
@@ -875,6 +881,7 @@ mod tests {
875881 use std:: path:: PathBuf ;
876882 use std:: sync:: Arc ;
877883 use url:: Url ;
884+ use uv_auth:: Credentials ;
878885 use uv_client:: BaseClientBuilder ;
879886 use uv_distribution_filename:: DistFilename ;
880887
@@ -958,8 +965,7 @@ mod tests {
958965 & filename,
959966 & Url :: parse ( "https://example.org/upload" ) . unwrap ( ) ,
960967 & BaseClientBuilder :: new ( ) . build ( ) ,
961- Some ( "ferris" ) ,
962- Some ( "F3RR!S" ) ,
968+ & Credentials :: basic ( Some ( "ferris" . to_string ( ) ) , Some ( "F3RR!S" . to_string ( ) ) ) ,
963969 & form_metadata,
964970 Arc :: new ( DummyReporter ) ,
965971 )
@@ -969,35 +975,35 @@ mod tests {
969975 insta:: with_settings!( {
970976 filters => [ ( "boundary=[0-9a-f-]+" , "boundary=[...]" ) ] ,
971977 } , {
972- assert_debug_snapshot!( & request, @r###"
973- RequestBuilder {
974- inner: RequestBuilder {
975- method: POST,
976- url: Url {
977- scheme: "https",
978- cannot_be_a_base: false,
979- username: "",
980- password: None,
981- host: Some(
982- Domain(
983- "example.org",
978+ assert_debug_snapshot!( & request, @r#"
979+ RequestBuilder {
980+ inner: RequestBuilder {
981+ method: POST,
982+ url: Url {
983+ scheme: "https",
984+ cannot_be_a_base: false,
985+ username: "",
986+ password: None,
987+ host: Some(
988+ Domain(
989+ "example.org",
990+ ),
984991 ),
985- ),
986- port: None,
987- path: "/upload",
988- query: None,
989- fragment: None,
992+ port: None,
993+ path: "/upload",
994+ query: None,
995+ fragment: None,
996+ },
997+ headers: {
998+ "content-type": "multipart/form-data; boundary=[...]",
999+ "content-length": "6803",
1000+ "accept": "application/json;q=0.9, text/plain;q=0.8, text/html;q=0.7",
1001+ "authorization": Sensitive,
1002+ },
9901003 },
991- headers: {
992- "content-type": "multipart/form-data; boundary=[...]",
993- "content-length": "6803",
994- "accept": "application/json;q=0.9, text/plain;q=0.8, text/html;q=0.7",
995- "authorization": "Basic ZmVycmlzOkYzUlIhUw==",
996- },
997- },
998- ..
999- }
1000- "### ) ;
1004+ ..
1005+ }
1006+ "# ) ;
10011007 } ) ;
10021008 }
10031009
@@ -1109,8 +1115,7 @@ mod tests {
11091115 & filename,
11101116 & Url :: parse ( "https://example.org/upload" ) . unwrap ( ) ,
11111117 & BaseClientBuilder :: new ( ) . build ( ) ,
1112- Some ( "ferris" ) ,
1113- Some ( "F3RR!S" ) ,
1118+ & Credentials :: basic ( Some ( "ferris" . to_string ( ) ) , Some ( "F3RR!S" . to_string ( ) ) ) ,
11141119 & form_metadata,
11151120 Arc :: new ( DummyReporter ) ,
11161121 )
@@ -1120,35 +1125,35 @@ mod tests {
11201125 insta:: with_settings!( {
11211126 filters => [ ( "boundary=[0-9a-f-]+" , "boundary=[...]" ) ] ,
11221127 } , {
1123- assert_debug_snapshot!( & request, @r###"
1124- RequestBuilder {
1125- inner: RequestBuilder {
1126- method: POST,
1127- url: Url {
1128- scheme: "https",
1129- cannot_be_a_base: false,
1130- username: "",
1131- password: None,
1132- host: Some(
1133- Domain(
1134- "example.org",
1128+ assert_debug_snapshot!( & request, @r#"
1129+ RequestBuilder {
1130+ inner: RequestBuilder {
1131+ method: POST,
1132+ url: Url {
1133+ scheme: "https",
1134+ cannot_be_a_base: false,
1135+ username: "",
1136+ password: None,
1137+ host: Some(
1138+ Domain(
1139+ "example.org",
1140+ ),
11351141 ),
1136- ),
1137- port: None,
1138- path: "/upload",
1139- query: None,
1140- fragment: None,
1142+ port: None,
1143+ path: "/upload",
1144+ query: None,
1145+ fragment: None,
1146+ },
1147+ headers: {
1148+ "content-type": "multipart/form-data; boundary=[...]",
1149+ "content-length": "19330",
1150+ "accept": "application/json;q=0.9, text/plain;q=0.8, text/html;q=0.7",
1151+ "authorization": Sensitive,
1152+ },
11411153 },
1142- headers: {
1143- "content-type": "multipart/form-data; boundary=[...]",
1144- "content-length": "19330",
1145- "accept": "application/json;q=0.9, text/plain;q=0.8, text/html;q=0.7",
1146- "authorization": "Basic ZmVycmlzOkYzUlIhUw==",
1147- },
1148- },
1149- ..
1150- }
1151- "### ) ;
1154+ ..
1155+ }
1156+ "# ) ;
11521157 } ) ;
11531158 }
11541159}
0 commit comments