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 } ;
@@ -43,6 +43,7 @@ use crate::views::{
43
43
EncodableCrate , EncodableCrateDependency , GoodCrate , PublishMetadata , PublishWarnings ,
44
44
} ;
45
45
use crates_io_diesel_helpers:: canon_crate_name;
46
+ use crates_io_trustpub:: access_token:: AccessToken ;
46
47
47
48
const MISSING_RIGHTS_ERROR_MESSAGE : & str = "this crate exists but you don't seem to be an owner. \
48
49
If you believe this is a mistake, perhaps you need \
@@ -51,6 +52,11 @@ const MISSING_RIGHTS_ERROR_MESSAGE: &str = "this crate exists but you don't seem
51
52
52
53
const MAX_DESCRIPTION_LENGTH : usize = 1000 ;
53
54
55
+ enum AuthType {
56
+ Regular ( Box < Authentication > ) ,
57
+ Oidc ( ) ,
58
+ }
59
+
54
60
/// Publish a new crate/version.
55
61
///
56
62
/// Used by `cargo publish` to publish a new crate or to publish a new version of an
@@ -130,30 +136,66 @@ pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult<Json<Go
130
136
None => EndpointScope :: PublishNew ,
131
137
} ;
132
138
133
- let auth = AuthCheck :: default ( )
134
- . with_endpoint_scope ( endpoint_scope)
135
- . for_crate ( & metadata. name )
136
- . check ( & req, & mut conn)
137
- . await ?;
139
+ let access_token = req
140
+ . headers
141
+ . get ( header:: AUTHORIZATION )
142
+ . and_then ( |h| h. as_bytes ( ) . strip_prefix ( b"Bearer " ) )
143
+ . filter ( |b| b. starts_with ( AccessToken :: PREFIX . as_bytes ( ) ) )
144
+ . map ( AccessToken :: from_byte_str)
145
+ . transpose ( )
146
+ . map_err ( |_| bad_request ( "Invalid authentication token" ) ) ?;
147
+
148
+ let auth = match ( access_token, & existing_crate) {
149
+ ( Some ( access_token) , Some ( existing_crate) ) => {
150
+ let hashed_token = access_token. sha256 ( ) ;
151
+
152
+ trustpub_tokens:: table
153
+ . filter ( trustpub_tokens:: crate_ids. contains ( vec ! [ existing_crate. id] ) )
154
+ . filter ( trustpub_tokens:: hashed_token. eq ( hashed_token. as_slice ( ) ) )
155
+ . filter ( trustpub_tokens:: expires_at. gt ( now) )
156
+ . select ( trustpub_tokens:: id)
157
+ . get_result :: < i64 > ( & mut conn)
158
+ . await ?;
159
+
160
+ AuthType :: Oidc ( )
161
+ }
162
+ _ => {
163
+ let auth = AuthCheck :: default ( )
164
+ . with_endpoint_scope ( endpoint_scope)
165
+ . for_crate ( & metadata. name )
166
+ . check ( & req, & mut conn)
167
+ . await ?;
168
+
169
+ AuthType :: Regular ( Box :: new ( auth) )
170
+ }
171
+ } ;
138
172
139
- let verified_email_address = auth. user ( ) . verified_email ( & mut conn) . await ?;
140
- let verified_email_address = verified_email_address. ok_or_else ( || {
141
- bad_request ( format ! (
142
- "A verified email address is required to publish crates to crates.io. \
173
+ let verified_email_address = match & auth {
174
+ AuthType :: Regular ( auth) => {
175
+ let verified_email_address = auth. user ( ) . verified_email ( & mut conn) . await ?;
176
+ let verified_email_address = verified_email_address. ok_or_else ( || {
177
+ bad_request ( format ! (
178
+ "A verified email address is required to publish crates to crates.io. \
143
179
Visit https://{}/settings/profile to set and verify your email address.",
144
- app. config. domain_name,
145
- ) )
146
- } ) ?;
147
-
148
- // Use a different rate limit whether this is a new or an existing crate.
149
- let rate_limit_action = match existing_crate {
150
- Some ( _) => LimitedAction :: PublishUpdate ,
151
- None => LimitedAction :: PublishNew ,
180
+ app. config. domain_name,
181
+ ) )
182
+ } ) ?;
183
+ Some ( verified_email_address)
184
+ }
185
+ AuthType :: Oidc ( ) => None ,
152
186
} ;
153
187
154
- app. rate_limiter
155
- . check_rate_limit ( auth. user ( ) . id , rate_limit_action, & mut conn)
156
- . await ?;
188
+ if let AuthType :: Regular ( auth) = & auth {
189
+ // Use a different rate limit whether this is a new or an existing crate.
190
+ let rate_limit_action = match existing_crate {
191
+ Some ( _) => LimitedAction :: PublishUpdate ,
192
+ None => LimitedAction :: PublishNew ,
193
+ } ;
194
+
195
+ app. rate_limiter
196
+ . check_rate_limit ( auth. user ( ) . id , rate_limit_action, & mut conn)
197
+ . await ?;
198
+ }
157
199
158
200
let max_upload_size = existing_crate
159
201
. as_ref ( )
@@ -342,9 +384,6 @@ pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult<Json<Go
342
384
validate_dependency ( dep) ?;
343
385
}
344
386
345
- let api_token_id = auth. api_token_id ( ) ;
346
- let user = auth. user ( ) ;
347
-
348
387
// Create a transaction on the database, if there are no errors,
349
388
// commit the transactions to record a new or updated crate.
350
389
conn. transaction ( |conn| async move {
@@ -368,17 +407,29 @@ pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult<Json<Go
368
407
return Err ( bad_request ( "cannot upload a crate with a reserved name" ) ) ;
369
408
}
370
409
371
- // To avoid race conditions, we try to insert
372
- // first so we know whether to add an owner
373
- let krate = match persist. create ( conn, user. id ) . await . optional ( ) ? {
374
- Some ( krate) => krate,
375
- None => persist. update ( conn) . await ?,
376
- } ;
410
+ let krate = match & auth {
411
+ AuthType :: Regular ( auth) => {
412
+ let user = auth. user ( ) ;
377
413
378
- let owners = krate. owners ( conn) . await ?;
379
- if Rights :: get ( user, & * app. github , & owners) . await ? < Rights :: Publish {
380
- return Err ( custom ( StatusCode :: FORBIDDEN , MISSING_RIGHTS_ERROR_MESSAGE ) ) ;
381
- }
414
+ // To avoid race conditions, we try to insert
415
+ // first so we know whether to add an owner
416
+ let krate = match persist. create ( conn, user. id ) . await . optional ( ) ? {
417
+ Some ( krate) => krate,
418
+ None => persist. update ( conn) . await ?,
419
+ } ;
420
+
421
+ let owners = krate. owners ( conn) . await ?;
422
+ if Rights :: get ( user, & * app. github , & owners) . await ? < Rights :: Publish {
423
+ return Err ( custom ( StatusCode :: FORBIDDEN , MISSING_RIGHTS_ERROR_MESSAGE ) ) ;
424
+ }
425
+
426
+ krate
427
+ }
428
+ AuthType :: Oidc ( ) => {
429
+ // OIDC does not support creating new crates
430
+ persist. update ( conn) . await ?
431
+ }
432
+ } ;
382
433
383
434
if krate. name != * name {
384
435
return Err ( bad_request ( format_args ! (
@@ -407,6 +458,11 @@ pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult<Json<Go
407
458
408
459
let edition = edition. map ( |edition| edition. as_str ( ) ) ;
409
460
461
+ let published_by = match & auth {
462
+ AuthType :: Regular ( auth) => Some ( auth. user ( ) . id ) ,
463
+ AuthType :: Oidc ( ) => None ,
464
+ } ;
465
+
410
466
// Read tarball from request
411
467
let hex_cksum: String = Sha256 :: digest ( & tarball_bytes) . encode_hex ( ) ;
412
468
@@ -417,7 +473,7 @@ pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult<Json<Go
417
473
// Downcast is okay because the file length must be less than the max upload size
418
474
// to get here, and max upload sizes are way less than i32 max
419
475
. size ( content_length as i32 )
420
- . published_by ( user . id )
476
+ . maybe_published_by ( published_by )
421
477
. checksum ( & hex_cksum)
422
478
. maybe_links ( package. links . as_deref ( ) )
423
479
. maybe_rust_version ( rust_version. as_deref ( ) )
@@ -432,7 +488,7 @@ pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult<Json<Go
432
488
. keywords ( & keywords)
433
489
. build ( ) ;
434
490
435
- let version = new_version. save ( conn, & verified_email_address) . await . map_err ( |error| {
491
+ let version = new_version. save ( conn, verified_email_address. as_deref ( ) ) . await . map_err ( |error| {
436
492
use diesel:: result:: { Error , DatabaseErrorKind } ;
437
493
match error {
438
494
Error :: DatabaseError ( DatabaseErrorKind :: UniqueViolation , _) =>
@@ -441,14 +497,16 @@ pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult<Json<Go
441
497
}
442
498
} ) ?;
443
499
444
- NewVersionOwnerAction :: builder ( )
445
- . version_id ( version. id )
446
- . user_id ( user. id )
447
- . maybe_api_token_id ( api_token_id)
448
- . action ( VersionAction :: Publish )
449
- . build ( )
450
- . insert ( conn)
451
- . await ?;
500
+ if let AuthType :: Regular ( auth) = & auth {
501
+ NewVersionOwnerAction :: builder ( )
502
+ . version_id ( version. id )
503
+ . user_id ( auth. user ( ) . id )
504
+ . maybe_api_token_id ( auth. api_token_id ( ) )
505
+ . action ( VersionAction :: Publish )
506
+ . build ( )
507
+ . insert ( conn)
508
+ . await ?;
509
+ }
452
510
453
511
// Link this new version to all dependencies
454
512
add_dependencies ( conn, & deps, version. id ) . await ?;
0 commit comments