11//! Functionality related to publishing a new crate or version of a crate.
22
33use crate :: app:: AppState ;
4- use crate :: auth:: AuthCheck ;
4+ use crate :: auth:: { AuthCheck , Authentication } ;
55use crate :: worker:: jobs:: {
66 self , CheckTyposquat , SendPublishNotificationsJob , UpdateDefaultVersion ,
77} ;
@@ -11,16 +11,16 @@ use cargo_manifest::{Dependency, DepsSet, TargetDepsSet};
1111use chrono:: { DateTime , SecondsFormat , Utc } ;
1212use crates_io_tarball:: { TarballError , process_tarball} ;
1313use crates_io_worker:: { BackgroundJob , EnqueueError } ;
14- use diesel:: dsl:: { exists, select} ;
14+ use diesel:: dsl:: { exists, now , select} ;
1515use diesel:: prelude:: * ;
1616use diesel:: sql_types:: Timestamptz ;
1717use diesel_async:: scoped_futures:: ScopedFutureExt ;
1818use diesel_async:: { AsyncConnection , AsyncPgConnection , RunQueryDsl } ;
1919use futures_util:: TryFutureExt ;
2020use futures_util:: TryStreamExt ;
2121use hex:: ToHex ;
22- use http:: StatusCode ;
2322use http:: request:: Parts ;
23+ use http:: { StatusCode , header} ;
2424use sha2:: { Digest , Sha256 } ;
2525use std:: collections:: HashMap ;
2626use tokio:: io:: { AsyncRead , AsyncReadExt } ;
@@ -38,12 +38,13 @@ use crate::middleware::log_request::RequestLogExt;
3838use crate :: models:: token:: EndpointScope ;
3939use crate :: rate_limiter:: LimitedAction ;
4040use crate :: schema:: * ;
41- use crate :: util:: errors:: { AppResult , BoxedAppError , bad_request, custom, internal} ;
41+ use crate :: util:: errors:: { AppResult , BoxedAppError , bad_request, custom, forbidden , internal} ;
4242use crate :: views:: {
4343 EncodableCrate , EncodableCrateDependency , GoodCrate , PublishMetadata , PublishWarnings ,
4444} ;
45- use crates_io_database:: models:: versions_published_by;
45+ use crates_io_database:: models:: { User , versions_published_by} ;
4646use crates_io_diesel_helpers:: canon_crate_name;
47+ use crates_io_trustpub:: access_token:: AccessToken ;
4748
4849const MISSING_RIGHTS_ERROR_MESSAGE : & str = "this crate exists but you don't seem to be an owner. \
4950 If you believe this is a mistake, perhaps you need \
@@ -52,6 +53,24 @@ const MISSING_RIGHTS_ERROR_MESSAGE: &str = "this crate exists but you don't seem
5253
5354const MAX_DESCRIPTION_LENGTH : usize = 1000 ;
5455
56+ enum AuthType {
57+ Regular ( Box < Authentication > ) ,
58+ TrustPub ,
59+ }
60+
61+ impl AuthType {
62+ fn user ( & self ) -> Option < & User > {
63+ match self {
64+ AuthType :: Regular ( auth) => Some ( auth. user ( ) ) ,
65+ AuthType :: TrustPub => None ,
66+ }
67+ }
68+
69+ fn user_id ( & self ) -> Option < i32 > {
70+ self . user ( ) . map ( |u| u. id )
71+ }
72+ }
73+
5574/// Publish a new crate/version.
5675///
5776/// Used by `cargo publish` to publish a new crate or to publish a new version of an
@@ -61,6 +80,7 @@ const MAX_DESCRIPTION_LENGTH: usize = 1000;
6180 path = "/api/v1/crates/new" ,
6281 security(
6382 ( "api_token" = [ ] ) ,
83+ ( "trustpub_token" = [ ] ) ,
6484 ( "cookie" = [ ] ) ,
6585 ) ,
6686 tag = "publish" ,
@@ -126,35 +146,79 @@ pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult<Json<Go
126146 . await
127147 . optional ( ) ?;
128148
129- let endpoint_scope = match existing_crate {
130- Some ( _) => EndpointScope :: PublishUpdate ,
131- None => EndpointScope :: PublishNew ,
132- } ;
149+ // Trusted publishing tokens are distinguished from regular crates.io API
150+ // tokens because they use the `Bearer` auth scheme, so we look for that
151+ // specific prefix.
152+ let trustpub_token = req
153+ . headers
154+ . get ( header:: AUTHORIZATION )
155+ . and_then ( |h| {
156+ let mut split = h. as_bytes ( ) . splitn ( 2 , |b| * b == b' ' ) ;
157+ Some ( ( split. next ( ) ?, split. next ( ) ?) )
158+ } )
159+ . filter ( |( scheme, _token) | scheme. eq_ignore_ascii_case ( b"Bearer" ) )
160+ . map ( |( _scheme, token) | token. trim_ascii ( ) )
161+ . map ( AccessToken :: from_byte_str)
162+ . transpose ( )
163+ . map_err ( |_| forbidden ( "Invalid authentication token" ) ) ?;
164+
165+ let auth = if let Some ( trustpub_token) = trustpub_token {
166+ let Some ( existing_crate) = & existing_crate else {
167+ let error = forbidden ( "Trusted Publishing tokens do not support creating new crates" ) ;
168+ return Err ( error) ;
169+ } ;
133170
134- let auth = AuthCheck :: default ( )
135- . with_endpoint_scope ( endpoint_scope)
136- . for_crate ( & metadata. name )
137- . check ( & req, & mut conn)
138- . await ?;
171+ let hashed_token = trustpub_token. sha256 ( ) ;
139172
140- let verified_email_address = auth. user ( ) . verified_email ( & mut conn) . await ?;
141- let verified_email_address = verified_email_address. ok_or_else ( || {
142- bad_request ( format ! (
143- "A verified email address is required to publish crates to crates.io. \
144- Visit https://{}/settings/profile to set and verify your email address.",
145- app. config. domain_name,
146- ) )
147- } ) ?;
173+ let crate_ids: Vec < Option < i32 > > = trustpub_tokens:: table
174+ . filter ( trustpub_tokens:: hashed_token. eq ( hashed_token. as_slice ( ) ) )
175+ . filter ( trustpub_tokens:: expires_at. gt ( now) )
176+ . select ( trustpub_tokens:: crate_ids)
177+ . get_result ( & mut conn)
178+ . await
179+ . optional ( ) ?
180+ . ok_or_else ( || forbidden ( "Invalid authentication token" ) ) ?;
181+
182+ if !crate_ids. contains ( & Some ( existing_crate. id ) ) {
183+ let name = & existing_crate. name ;
184+ let error = format ! ( "The provided access token is not valid for crate `{name}`" ) ;
185+ return Err ( forbidden ( error) ) ;
186+ }
187+
188+ AuthType :: TrustPub
189+ } else {
190+ let endpoint_scope = match existing_crate {
191+ Some ( _) => EndpointScope :: PublishUpdate ,
192+ None => EndpointScope :: PublishNew ,
193+ } ;
194+
195+ let auth = AuthCheck :: default ( )
196+ . with_endpoint_scope ( endpoint_scope)
197+ . for_crate ( & metadata. name )
198+ . check ( & req, & mut conn)
199+ . await ?;
148200
149- // Use a different rate limit whether this is a new or an existing crate.
150- let rate_limit_action = match existing_crate {
151- Some ( _) => LimitedAction :: PublishUpdate ,
152- None => LimitedAction :: PublishNew ,
201+ AuthType :: Regular ( Box :: new ( auth) )
153202 } ;
154203
155- app. rate_limiter
156- . check_rate_limit ( auth. user ( ) . id , rate_limit_action, & mut conn)
157- . await ?;
204+ let verified_email_address = if let Some ( user) = auth. user ( ) {
205+ let verified_email_address = user. verified_email ( & mut conn) . await ?;
206+ Some ( verified_email_address. ok_or_else ( || verified_email_error ( & app. config . domain_name ) ) ?)
207+ } else {
208+ None
209+ } ;
210+
211+ if let Some ( user_id) = auth. user_id ( ) {
212+ // Use a different rate limit whether this is a new or an existing crate.
213+ let rate_limit_action = match existing_crate {
214+ Some ( _) => LimitedAction :: PublishUpdate ,
215+ None => LimitedAction :: PublishNew ,
216+ } ;
217+
218+ app. rate_limiter
219+ . check_rate_limit ( user_id, rate_limit_action, & mut conn)
220+ . await ?;
221+ }
158222
159223 let max_upload_size = existing_crate
160224 . as_ref ( )
@@ -343,9 +407,6 @@ pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult<Json<Go
343407 validate_dependency ( dep) ?;
344408 }
345409
346- let api_token_id = auth. api_token_id ( ) ;
347- let user = auth. user ( ) ;
348-
349410 // Create a transaction on the database, if there are no errors,
350411 // commit the transactions to record a new or updated crate.
351412 conn. transaction ( |conn| async move {
@@ -369,17 +430,24 @@ pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult<Json<Go
369430 return Err ( bad_request ( "cannot upload a crate with a reserved name" ) ) ;
370431 }
371432
372- // To avoid race conditions, we try to insert
373- // first so we know whether to add an owner
374- let krate = match persist. create ( conn, user. id ) . await . optional ( ) ? {
375- Some ( krate) => krate,
376- None => persist. update ( conn) . await ?,
377- } ;
433+ let krate = if let Some ( user) = auth. user ( ) {
434+ // To avoid race conditions, we try to insert
435+ // first so we know whether to add an owner
436+ let krate = match persist. create ( conn, user. id ) . await . optional ( ) ? {
437+ Some ( krate) => krate,
438+ None => persist. update ( conn) . await ?,
439+ } ;
378440
379- let owners = krate. owners ( conn) . await ?;
380- if Rights :: get ( user, & * app. github , & owners) . await ? < Rights :: Publish {
381- return Err ( custom ( StatusCode :: FORBIDDEN , MISSING_RIGHTS_ERROR_MESSAGE ) ) ;
382- }
441+ let owners = krate. owners ( conn) . await ?;
442+ if Rights :: get ( user, & * app. github , & owners) . await ? < Rights :: Publish {
443+ return Err ( custom ( StatusCode :: FORBIDDEN , MISSING_RIGHTS_ERROR_MESSAGE ) ) ;
444+ }
445+
446+ krate
447+ } else {
448+ // Trusted Publishing does not support creating new crates
449+ persist. update ( conn) . await ?
450+ } ;
383451
384452 if krate. name != * name {
385453 return Err ( bad_request ( format_args ! (
@@ -418,7 +486,7 @@ pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult<Json<Go
418486 // Downcast is okay because the file length must be less than the max upload size
419487 // to get here, and max upload sizes are way less than i32 max
420488 . size ( content_length as i32 )
421- . published_by ( user . id )
489+ . maybe_published_by ( auth . user_id ( ) )
422490 . checksum ( & hex_cksum)
423491 . maybe_links ( package. links . as_deref ( ) )
424492 . maybe_rust_version ( rust_version. as_deref ( ) )
@@ -442,16 +510,20 @@ pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult<Json<Go
442510 }
443511 } ) ?;
444512
445- versions_published_by:: insert ( version. id , & verified_email_address, conn) . await ?;
513+ if let Some ( email_address) = verified_email_address {
514+ versions_published_by:: insert ( version. id , & email_address, conn) . await ?;
515+ }
446516
447- NewVersionOwnerAction :: builder ( )
448- . version_id ( version. id )
449- . user_id ( user. id )
450- . maybe_api_token_id ( api_token_id)
451- . action ( VersionAction :: Publish )
452- . build ( )
453- . insert ( conn)
454- . await ?;
517+ if let AuthType :: Regular ( auth) = & auth {
518+ NewVersionOwnerAction :: builder ( )
519+ . version_id ( version. id )
520+ . user_id ( auth. user ( ) . id )
521+ . maybe_api_token_id ( auth. api_token_id ( ) )
522+ . action ( VersionAction :: Publish )
523+ . build ( )
524+ . insert ( conn)
525+ . await ?;
526+ }
455527
456528 // Link this new version to all dependencies
457529 add_dependencies ( conn, & deps, version. id ) . await ?;
@@ -464,7 +536,7 @@ pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult<Json<Go
464536 . await
465537 . optional ( ) ?;
466538
467- let num_versions = existing_default_version. as_ref ( ) . and_then ( |t|t. 1 ) . unwrap_or_default ( ) ;
539+ let num_versions = existing_default_version. as_ref ( ) . and_then ( |t| t. 1 ) . unwrap_or_default ( ) ;
468540 let mut default_version = None ;
469541 // Upsert the `default_value` determined by the existing `default_value` and the
470542 // published version. Note that this could potentially write an outdated version
@@ -728,6 +800,13 @@ fn validate_rust_version(value: &str) -> AppResult<()> {
728800 }
729801}
730802
803+ fn verified_email_error ( domain : & str ) -> BoxedAppError {
804+ bad_request ( format ! (
805+ "A verified email address is required to publish crates to crates.io. \
806+ Visit https://{domain}/settings/profile to set and verify your email address.",
807+ ) )
808+ }
809+
731810fn convert_dependencies (
732811 normal_deps : Option < & DepsSet > ,
733812 dev_deps : Option < & DepsSet > ,
0 commit comments