1
1
//! Functionality related to publishing a new crate or version of a crate.
2
2
3
3
use crate :: app:: AppState ;
4
- use crate :: auth:: AuthCheck ;
4
+ use crate :: auth:: { AuthCheck , Authentication } ;
5
5
use crate :: worker:: jobs:: {
6
6
self , CheckTyposquat , SendPublishNotificationsJob , UpdateDefaultVersion ,
7
7
} ;
@@ -11,16 +11,16 @@ use cargo_manifest::{Dependency, DepsSet, TargetDepsSet};
11
11
use chrono:: { DateTime , SecondsFormat , Utc } ;
12
12
use crates_io_tarball:: { TarballError , process_tarball} ;
13
13
use crates_io_worker:: { BackgroundJob , EnqueueError } ;
14
- use diesel:: dsl:: { exists, select} ;
14
+ use diesel:: dsl:: { exists, now , select} ;
15
15
use diesel:: prelude:: * ;
16
16
use diesel:: sql_types:: Timestamptz ;
17
17
use diesel_async:: scoped_futures:: ScopedFutureExt ;
18
18
use diesel_async:: { AsyncConnection , AsyncPgConnection , RunQueryDsl } ;
19
19
use futures_util:: TryFutureExt ;
20
20
use futures_util:: TryStreamExt ;
21
21
use hex:: ToHex ;
22
- use http:: StatusCode ;
23
22
use http:: request:: Parts ;
23
+ use http:: { StatusCode , header} ;
24
24
use sha2:: { Digest , Sha256 } ;
25
25
use std:: collections:: HashMap ;
26
26
use tokio:: io:: { AsyncRead , AsyncReadExt } ;
@@ -38,12 +38,13 @@ use crate::middleware::log_request::RequestLogExt;
38
38
use crate :: models:: token:: EndpointScope ;
39
39
use crate :: rate_limiter:: LimitedAction ;
40
40
use 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} ;
42
42
use crate :: views:: {
43
43
EncodableCrate , EncodableCrateDependency , GoodCrate , PublishMetadata , PublishWarnings ,
44
44
} ;
45
- use crates_io_database:: models:: versions_published_by;
45
+ use crates_io_database:: models:: { User , versions_published_by} ;
46
46
use crates_io_diesel_helpers:: canon_crate_name;
47
+ use crates_io_trustpub:: access_token:: AccessToken ;
47
48
48
49
const MISSING_RIGHTS_ERROR_MESSAGE : & str = "this crate exists but you don't seem to be an owner. \
49
50
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
52
53
53
54
const MAX_DESCRIPTION_LENGTH : usize = 1000 ;
54
55
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
+
55
74
/// Publish a new crate/version.
56
75
///
57
76
/// 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;
61
80
path = "/api/v1/crates/new" ,
62
81
security(
63
82
( "api_token" = [ ] ) ,
83
+ ( "trustpub_token" = [ ] ) ,
64
84
( "cookie" = [ ] ) ,
65
85
) ,
66
86
tag = "publish" ,
@@ -126,35 +146,76 @@ pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult<Json<Go
126
146
. await
127
147
. optional ( ) ?;
128
148
129
- let endpoint_scope = match existing_crate {
130
- Some ( _) => EndpointScope :: PublishUpdate ,
131
- None => EndpointScope :: PublishNew ,
132
- } ;
149
+ let trustpub_token = req
150
+ . headers
151
+ . get ( header:: AUTHORIZATION )
152
+ . and_then ( |h| {
153
+ let mut split = h. as_bytes ( ) . splitn ( 2 , |b| * b == b' ' ) ;
154
+ Some ( ( split. next ( ) ?, split. next ( ) ?) )
155
+ } )
156
+ . filter ( |( scheme, _token) | scheme. eq_ignore_ascii_case ( b"Bearer" ) )
157
+ . map ( |( _scheme, token) | token. trim_ascii ( ) )
158
+ . map ( AccessToken :: from_byte_str)
159
+ . transpose ( )
160
+ . map_err ( |_| forbidden ( "Invalid authentication token" ) ) ?;
161
+
162
+ let auth = if let Some ( trustpub_token) = trustpub_token {
163
+ let Some ( existing_crate) = & existing_crate else {
164
+ let error = forbidden ( "Trusted Publishing tokens do not support creating new crates" ) ;
165
+ return Err ( error) ;
166
+ } ;
133
167
134
- let auth = AuthCheck :: default ( )
135
- . with_endpoint_scope ( endpoint_scope)
136
- . for_crate ( & metadata. name )
137
- . check ( & req, & mut conn)
138
- . await ?;
168
+ let hashed_token = trustpub_token. sha256 ( ) ;
139
169
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
- } ) ?;
170
+ let crate_ids: Vec < Option < i32 > > = trustpub_tokens:: table
171
+ . filter ( trustpub_tokens:: hashed_token. eq ( hashed_token. as_slice ( ) ) )
172
+ . filter ( trustpub_tokens:: expires_at. gt ( now) )
173
+ . select ( trustpub_tokens:: crate_ids)
174
+ . get_result ( & mut conn)
175
+ . await
176
+ . optional ( ) ?
177
+ . ok_or_else ( || forbidden ( "Invalid authentication token" ) ) ?;
178
+
179
+ if !crate_ids. contains ( & Some ( existing_crate. id ) ) {
180
+ let name = & existing_crate. name ;
181
+ let error = format ! ( "The provided access token is not valid for crate `{name}`" ) ;
182
+ return Err ( forbidden ( error) ) ;
183
+ }
184
+
185
+ AuthType :: TrustPub
186
+ } else {
187
+ let endpoint_scope = match existing_crate {
188
+ Some ( _) => EndpointScope :: PublishUpdate ,
189
+ None => EndpointScope :: PublishNew ,
190
+ } ;
191
+
192
+ let auth = AuthCheck :: default ( )
193
+ . with_endpoint_scope ( endpoint_scope)
194
+ . for_crate ( & metadata. name )
195
+ . check ( & req, & mut conn)
196
+ . await ?;
148
197
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 ,
198
+ AuthType :: Regular ( Box :: new ( auth) )
153
199
} ;
154
200
155
- app. rate_limiter
156
- . check_rate_limit ( auth. user ( ) . id , rate_limit_action, & mut conn)
157
- . await ?;
201
+ let verified_email_address = if let Some ( user) = auth. user ( ) {
202
+ let verified_email_address = user. verified_email ( & mut conn) . await ?;
203
+ Some ( verified_email_address. ok_or_else ( || verified_email_error ( & app. config . domain_name ) ) ?)
204
+ } else {
205
+ None
206
+ } ;
207
+
208
+ if let Some ( user_id) = auth. user_id ( ) {
209
+ // Use a different rate limit whether this is a new or an existing crate.
210
+ let rate_limit_action = match existing_crate {
211
+ Some ( _) => LimitedAction :: PublishUpdate ,
212
+ None => LimitedAction :: PublishNew ,
213
+ } ;
214
+
215
+ app. rate_limiter
216
+ . check_rate_limit ( user_id, rate_limit_action, & mut conn)
217
+ . await ?;
218
+ }
158
219
159
220
let max_upload_size = existing_crate
160
221
. as_ref ( )
@@ -343,9 +404,6 @@ pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult<Json<Go
343
404
validate_dependency ( dep) ?;
344
405
}
345
406
346
- let api_token_id = auth. api_token_id ( ) ;
347
- let user = auth. user ( ) ;
348
-
349
407
// Create a transaction on the database, if there are no errors,
350
408
// commit the transactions to record a new or updated crate.
351
409
conn. transaction ( |conn| async move {
@@ -369,17 +427,24 @@ pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult<Json<Go
369
427
return Err ( bad_request ( "cannot upload a crate with a reserved name" ) ) ;
370
428
}
371
429
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
- } ;
430
+ let krate = if let Some ( user) = auth. user ( ) {
431
+ // To avoid race conditions, we try to insert
432
+ // first so we know whether to add an owner
433
+ let krate = match persist. create ( conn, user. id ) . await . optional ( ) ? {
434
+ Some ( krate) => krate,
435
+ None => persist. update ( conn) . await ?,
436
+ } ;
378
437
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
- }
438
+ let owners = krate. owners ( conn) . await ?;
439
+ if Rights :: get ( user, & * app. github , & owners) . await ? < Rights :: Publish {
440
+ return Err ( custom ( StatusCode :: FORBIDDEN , MISSING_RIGHTS_ERROR_MESSAGE ) ) ;
441
+ }
442
+
443
+ krate
444
+ } else {
445
+ // Trusted Publishing does not support creating new crates
446
+ persist. update ( conn) . await ?
447
+ } ;
383
448
384
449
if krate. name != * name {
385
450
return Err ( bad_request ( format_args ! (
@@ -418,7 +483,7 @@ pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult<Json<Go
418
483
// Downcast is okay because the file length must be less than the max upload size
419
484
// to get here, and max upload sizes are way less than i32 max
420
485
. size ( content_length as i32 )
421
- . published_by ( user . id )
486
+ . maybe_published_by ( auth . user_id ( ) )
422
487
. checksum ( & hex_cksum)
423
488
. maybe_links ( package. links . as_deref ( ) )
424
489
. maybe_rust_version ( rust_version. as_deref ( ) )
@@ -442,16 +507,20 @@ pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult<Json<Go
442
507
}
443
508
} ) ?;
444
509
445
- versions_published_by:: insert ( version. id , & verified_email_address, conn) . await ?;
510
+ if let Some ( email_address) = verified_email_address {
511
+ versions_published_by:: insert ( version. id , & email_address, conn) . await ?;
512
+ }
446
513
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 ?;
514
+ if let AuthType :: Regular ( auth) = & auth {
515
+ NewVersionOwnerAction :: builder ( )
516
+ . version_id ( version. id )
517
+ . user_id ( auth. user ( ) . id )
518
+ . maybe_api_token_id ( auth. api_token_id ( ) )
519
+ . action ( VersionAction :: Publish )
520
+ . build ( )
521
+ . insert ( conn)
522
+ . await ?;
523
+ }
455
524
456
525
// Link this new version to all dependencies
457
526
add_dependencies ( conn, & deps, version. id ) . await ?;
@@ -464,7 +533,7 @@ pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult<Json<Go
464
533
. await
465
534
. optional ( ) ?;
466
535
467
- let num_versions = existing_default_version. as_ref ( ) . and_then ( |t|t. 1 ) . unwrap_or_default ( ) ;
536
+ let num_versions = existing_default_version. as_ref ( ) . and_then ( |t| t. 1 ) . unwrap_or_default ( ) ;
468
537
let mut default_version = None ;
469
538
// Upsert the `default_value` determined by the existing `default_value` and the
470
539
// published version. Note that this could potentially write an outdated version
@@ -728,6 +797,13 @@ fn validate_rust_version(value: &str) -> AppResult<()> {
728
797
}
729
798
}
730
799
800
+ fn verified_email_error ( domain : & str ) -> BoxedAppError {
801
+ bad_request ( format ! (
802
+ "A verified email address is required to publish crates to crates.io. \
803
+ Visit https://{domain}/settings/profile to set and verify your email address.",
804
+ ) )
805
+ }
806
+
731
807
fn convert_dependencies (
732
808
normal_deps : Option < & DepsSet > ,
733
809
dev_deps : Option < & DepsSet > ,
0 commit comments