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,79 @@ 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
+ // 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
+ } ;
133
170
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 ( ) ;
139
172
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 ?;
148
200
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) )
153
202
} ;
154
203
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
+ }
158
222
159
223
let max_upload_size = existing_crate
160
224
. as_ref ( )
@@ -343,9 +407,6 @@ pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult<Json<Go
343
407
validate_dependency ( dep) ?;
344
408
}
345
409
346
- let api_token_id = auth. api_token_id ( ) ;
347
- let user = auth. user ( ) ;
348
-
349
410
// Create a transaction on the database, if there are no errors,
350
411
// commit the transactions to record a new or updated crate.
351
412
conn. transaction ( |conn| async move {
@@ -369,17 +430,24 @@ pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult<Json<Go
369
430
return Err ( bad_request ( "cannot upload a crate with a reserved name" ) ) ;
370
431
}
371
432
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
+ } ;
378
440
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
+ } ;
383
451
384
452
if krate. name != * name {
385
453
return Err ( bad_request ( format_args ! (
@@ -418,7 +486,7 @@ pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult<Json<Go
418
486
// Downcast is okay because the file length must be less than the max upload size
419
487
// to get here, and max upload sizes are way less than i32 max
420
488
. size ( content_length as i32 )
421
- . published_by ( user . id )
489
+ . maybe_published_by ( auth . user_id ( ) )
422
490
. checksum ( & hex_cksum)
423
491
. maybe_links ( package. links . as_deref ( ) )
424
492
. maybe_rust_version ( rust_version. as_deref ( ) )
@@ -442,16 +510,20 @@ pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult<Json<Go
442
510
}
443
511
} ) ?;
444
512
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
+ }
446
516
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
+ }
455
527
456
528
// Link this new version to all dependencies
457
529
add_dependencies ( conn, & deps, version. id ) . await ?;
@@ -464,7 +536,7 @@ pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult<Json<Go
464
536
. await
465
537
. optional ( ) ?;
466
538
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 ( ) ;
468
540
let mut default_version = None ;
469
541
// Upsert the `default_value` determined by the existing `default_value` and the
470
542
// published version. Note that this could potentially write an outdated version
@@ -728,6 +800,13 @@ fn validate_rust_version(value: &str) -> AppResult<()> {
728
800
}
729
801
}
730
802
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
+
731
810
fn convert_dependencies (
732
811
normal_deps : Option < & DepsSet > ,
733
812
dev_deps : Option < & DepsSet > ,
0 commit comments