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,69 @@ 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 access_token = req
150
+ . headers
151
+ . get ( header:: AUTHORIZATION )
152
+ . and_then ( |h| h. as_bytes ( ) . strip_prefix ( b"Bearer " ) )
153
+ . map ( AccessToken :: from_byte_str)
154
+ . transpose ( )
155
+ . map_err ( |_| forbidden ( "Invalid authentication token" ) ) ?;
156
+
157
+ let auth = match ( access_token, & existing_crate) {
158
+ ( Some ( access_token) , Some ( existing_crate) ) => {
159
+ let hashed_token = access_token. sha256 ( ) ;
160
+
161
+ let crate_ids: Vec < Option < i32 > > = trustpub_tokens:: table
162
+ . filter ( trustpub_tokens:: hashed_token. eq ( hashed_token. as_slice ( ) ) )
163
+ . filter ( trustpub_tokens:: expires_at. gt ( now) )
164
+ . select ( trustpub_tokens:: crate_ids)
165
+ . get_result ( & mut conn)
166
+ . await
167
+ . optional ( ) ?
168
+ . ok_or_else ( || forbidden ( "Invalid authentication token" ) ) ?;
169
+
170
+ if !crate_ids. contains ( & Some ( existing_crate. id ) ) {
171
+ let name = & existing_crate. name ;
172
+ let error = format ! ( "The provided access token is not valid for crate `{name}`" ) ;
173
+ return Err ( forbidden ( error) ) ;
174
+ }
133
175
134
- let auth = AuthCheck :: default ( )
135
- . with_endpoint_scope ( endpoint_scope)
136
- . for_crate ( & metadata. name )
137
- . check ( & req, & mut conn)
138
- . await ?;
176
+ AuthType :: TrustPub
177
+ }
178
+ _ => {
179
+ let endpoint_scope = match existing_crate {
180
+ Some ( _) => EndpointScope :: PublishUpdate ,
181
+ None => EndpointScope :: PublishNew ,
182
+ } ;
139
183
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
- } ) ?;
184
+ let auth = AuthCheck :: default ( )
185
+ . with_endpoint_scope ( endpoint_scope)
186
+ . for_crate ( & metadata. name )
187
+ . check ( & req, & mut conn)
188
+ . await ?;
148
189
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 ,
190
+ AuthType :: Regular ( Box :: new ( auth) )
191
+ }
153
192
} ;
154
193
155
- app. rate_limiter
156
- . check_rate_limit ( auth. user ( ) . id , rate_limit_action, & mut conn)
157
- . await ?;
194
+ let verified_email_address = if let Some ( user) = auth. user ( ) {
195
+ let verified_email_address = user. verified_email ( & mut conn) . await ?;
196
+ Some ( verified_email_address. ok_or_else ( || verified_email_error ( & app. config . domain_name ) ) ?)
197
+ } else {
198
+ None
199
+ } ;
200
+
201
+ if let Some ( user_id) = auth. user_id ( ) {
202
+ // Use a different rate limit whether this is a new or an existing crate.
203
+ let rate_limit_action = match existing_crate {
204
+ Some ( _) => LimitedAction :: PublishUpdate ,
205
+ None => LimitedAction :: PublishNew ,
206
+ } ;
207
+
208
+ app. rate_limiter
209
+ . check_rate_limit ( user_id, rate_limit_action, & mut conn)
210
+ . await ?;
211
+ }
158
212
159
213
let max_upload_size = existing_crate
160
214
. as_ref ( )
@@ -343,9 +397,6 @@ pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult<Json<Go
343
397
validate_dependency ( dep) ?;
344
398
}
345
399
346
- let api_token_id = auth. api_token_id ( ) ;
347
- let user = auth. user ( ) ;
348
-
349
400
// Create a transaction on the database, if there are no errors,
350
401
// commit the transactions to record a new or updated crate.
351
402
conn. transaction ( |conn| async move {
@@ -369,17 +420,24 @@ pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult<Json<Go
369
420
return Err ( bad_request ( "cannot upload a crate with a reserved name" ) ) ;
370
421
}
371
422
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
- } ;
423
+ let krate = if let Some ( user) = auth. user ( ) {
424
+ // To avoid race conditions, we try to insert
425
+ // first so we know whether to add an owner
426
+ let krate = match persist. create ( conn, user. id ) . await . optional ( ) ? {
427
+ Some ( krate) => krate,
428
+ None => persist. update ( conn) . await ?,
429
+ } ;
378
430
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
- }
431
+ let owners = krate. owners ( conn) . await ?;
432
+ if Rights :: get ( user, & * app. github , & owners) . await ? < Rights :: Publish {
433
+ return Err ( custom ( StatusCode :: FORBIDDEN , MISSING_RIGHTS_ERROR_MESSAGE ) ) ;
434
+ }
435
+
436
+ krate
437
+ } else {
438
+ // Trusted Publishing does not support creating new crates
439
+ persist. update ( conn) . await ?
440
+ } ;
383
441
384
442
if krate. name != * name {
385
443
return Err ( bad_request ( format_args ! (
@@ -418,7 +476,7 @@ pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult<Json<Go
418
476
// Downcast is okay because the file length must be less than the max upload size
419
477
// to get here, and max upload sizes are way less than i32 max
420
478
. size ( content_length as i32 )
421
- . published_by ( user . id )
479
+ . maybe_published_by ( auth . user_id ( ) )
422
480
. checksum ( & hex_cksum)
423
481
. maybe_links ( package. links . as_deref ( ) )
424
482
. maybe_rust_version ( rust_version. as_deref ( ) )
@@ -442,16 +500,20 @@ pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult<Json<Go
442
500
}
443
501
} ) ?;
444
502
445
- versions_published_by:: insert ( version. id , & verified_email_address, conn) . await ?;
503
+ if let Some ( email_address) = verified_email_address {
504
+ versions_published_by:: insert ( version. id , & email_address, conn) . await ?;
505
+ }
446
506
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 ?;
507
+ if let AuthType :: Regular ( auth) = & auth {
508
+ NewVersionOwnerAction :: builder ( )
509
+ . version_id ( version. id )
510
+ . user_id ( auth. user ( ) . id )
511
+ . maybe_api_token_id ( auth. api_token_id ( ) )
512
+ . action ( VersionAction :: Publish )
513
+ . build ( )
514
+ . insert ( conn)
515
+ . await ?;
516
+ }
455
517
456
518
// Link this new version to all dependencies
457
519
add_dependencies ( conn, & deps, version. id ) . await ?;
@@ -728,6 +790,13 @@ fn validate_rust_version(value: &str) -> AppResult<()> {
728
790
}
729
791
}
730
792
793
+ fn verified_email_error ( domain : & str ) -> BoxedAppError {
794
+ bad_request ( format ! (
795
+ "A verified email address is required to publish crates to crates.io. \
796
+ Visit https://{domain}/settings/profile to set and verify your email address.",
797
+ ) )
798
+ }
799
+
731
800
fn convert_dependencies (
732
801
normal_deps : Option < & DepsSet > ,
733
802
dev_deps : Option < & DepsSet > ,
0 commit comments