Skip to content

Commit c12a5c6

Browse files
committed
publish: Block token-based publishes for crates requiring trusted publishing
1 parent a5d14e4 commit c12a5c6

File tree

4 files changed

+99
-3
lines changed

4 files changed

+99
-3
lines changed

src/controllers/krate/publish.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,16 @@ pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult<Json<Go
216216
AuthType::Regular(Box::new(auth))
217217
};
218218

219+
// Check if crate requires trusted publishing
220+
if let Some(existing_crate) = &existing_crate
221+
&& existing_crate.trustpub_only
222+
&& matches!(auth, AuthType::Regular(_))
223+
{
224+
return Err(forbidden(
225+
"You tried to publish with an API token but this crate requires trusted publishing.",
226+
));
227+
}
228+
219229
let verified_email_address = if let Some(user) = auth.user() {
220230
let verified_email_address = user.verified_email(&mut conn).await?;
221231
Some(verified_email_address.ok_or_else(|| verified_email_error(&app.config.domain_name))?)

src/tests/krate/publish/auth.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::builders::{CrateBuilder, PublishBuilder};
22
use crate::util::{MockTokenUser, RequestHelper, TestApp};
3-
use crates_io::schema::api_tokens;
3+
use crates_io::schema::{api_tokens, crates};
44
use diesel::ExpressionMethods;
55
use diesel_async::RunQueryDsl;
66
use googletest::prelude::*;
@@ -78,3 +78,31 @@ async fn new_krate_with_bearer_token() {
7878
rss/updates.xml
7979
");
8080
}
81+
82+
#[tokio::test(flavor = "multi_thread")]
83+
async fn publish_with_token_rejected_when_trustpub_only() {
84+
let (app, _, user, token) = TestApp::full().with_token().await;
85+
let mut conn = app.db_conn().await;
86+
87+
// Create a crate
88+
CrateBuilder::new("foo_trustpub_only", user.as_model().id)
89+
.expect_build(&mut conn)
90+
.await;
91+
92+
// Set trustpub_only to true
93+
diesel::update(crates::table)
94+
.filter(crates::name.eq("foo_trustpub_only"))
95+
.set(crates::trustpub_only.eq(true))
96+
.execute(&mut conn)
97+
.await
98+
.unwrap();
99+
100+
// Try to publish with API token - should be rejected
101+
let crate_to_publish = PublishBuilder::new("foo_trustpub_only", "1.0.0");
102+
let response = token.publish_crate(crate_to_publish).await;
103+
assert_snapshot!(response.status(), @"403 Forbidden");
104+
assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"You tried to publish with an API token but this crate requires trusted publishing."}]}"#);
105+
106+
assert_that!(app.stored_files().await, is_empty());
107+
assert_that!(app.emails().await, is_empty());
108+
}

src/tests/krate/publish/trustpub_github.rs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::builders::{CrateBuilder, PublishBuilder};
22
use crate::util::{MockTokenUser, RequestHelper, TestApp};
33
use chrono::{TimeDelta, Utc};
4+
use crates_io::schema::crates;
45
use crates_io_database::models::trustpub::NewToken;
56
use crates_io_github::{GitHubUser, MockGitHubClient};
67
use crates_io_trustpub::access_token::AccessToken;
@@ -9,7 +10,8 @@ use crates_io_trustpub::github::test_helpers::FullGitHubClaims;
910
use crates_io_trustpub::keystore::MockOidcKeyStore;
1011
use crates_io_trustpub::test_keys::encode_for_testing;
1112
use diesel::QueryResult;
12-
use diesel_async::AsyncPgConnection;
13+
use diesel::prelude::*;
14+
use diesel_async::{AsyncPgConnection, RunQueryDsl};
1315
use insta::{assert_json_snapshot, assert_snapshot};
1416
use mockall::predicate::*;
1517
use p256::ecdsa::signature::digest::Output;
@@ -325,3 +327,30 @@ async fn test_token_for_wrong_crate() -> anyhow::Result<()> {
325327

326328
Ok(())
327329
}
330+
331+
#[tokio::test(flavor = "multi_thread")]
332+
async fn test_trustpub_works_when_trustpub_only_enabled() -> anyhow::Result<()> {
333+
let (app, _client, cookie_client) = TestApp::full().with_user().await;
334+
335+
let mut conn = app.db_conn().await;
336+
337+
let owner_id = cookie_client.as_model().id;
338+
let krate = CrateBuilder::new("foo", owner_id).build(&mut conn).await?;
339+
340+
// Set trustpub_only to true
341+
diesel::update(crates::table)
342+
.filter(crates::name.eq(&krate.name))
343+
.set(crates::trustpub_only.eq(true))
344+
.execute(&mut conn)
345+
.await?;
346+
347+
let token = new_token(&mut conn, krate.id).await?;
348+
let oidc_token_client = MockTokenUser::with_auth_header(token, app.clone());
349+
350+
// Publishing with trusted publishing should work
351+
let pb = PublishBuilder::new(&krate.name, "1.1.0");
352+
let response = oidc_token_client.publish_crate(pb).await;
353+
assert_snapshot!(response.status(), @"200 OK");
354+
355+
Ok(())
356+
}

src/tests/krate/publish/trustpub_gitlab.rs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
use crate::builders::{CrateBuilder, PublishBuilder};
22
use crate::util::{MockTokenUser, RequestHelper, TestApp};
33
use chrono::{TimeDelta, Utc};
4+
use crates_io::schema::crates;
45
use crates_io_database::models::trustpub::NewToken;
56
use crates_io_trustpub::access_token::AccessToken;
67
use crates_io_trustpub::gitlab::GITLAB_ISSUER_URL;
78
use crates_io_trustpub::gitlab::test_helpers::FullGitLabClaims;
89
use crates_io_trustpub::keystore::MockOidcKeyStore;
910
use crates_io_trustpub::test_keys::encode_for_testing;
1011
use diesel::QueryResult;
11-
use diesel_async::AsyncPgConnection;
12+
use diesel::prelude::*;
13+
use diesel_async::{AsyncPgConnection, RunQueryDsl};
1214
use insta::{assert_json_snapshot, assert_snapshot};
1315
use p256::ecdsa::signature::digest::Output;
1416
use secrecy::ExposeSecret;
@@ -306,3 +308,30 @@ async fn test_token_for_wrong_crate() -> anyhow::Result<()> {
306308

307309
Ok(())
308310
}
311+
312+
#[tokio::test(flavor = "multi_thread")]
313+
async fn test_trustpub_works_when_trustpub_only_enabled() -> anyhow::Result<()> {
314+
let (app, _client, cookie_client) = TestApp::full().with_user().await;
315+
316+
let mut conn = app.db_conn().await;
317+
318+
let owner_id = cookie_client.as_model().id;
319+
let krate = CrateBuilder::new("foo", owner_id).build(&mut conn).await?;
320+
321+
// Set trustpub_only to true
322+
diesel::update(crates::table)
323+
.filter(crates::name.eq(&krate.name))
324+
.set(crates::trustpub_only.eq(true))
325+
.execute(&mut conn)
326+
.await?;
327+
328+
let token = new_token(&mut conn, krate.id).await?;
329+
let oidc_token_client = MockTokenUser::with_auth_header(token, app.clone());
330+
331+
// Publishing with trusted publishing should work
332+
let pb = PublishBuilder::new(&krate.name, "1.1.0");
333+
let response = oidc_token_client.publish_crate(pb).await;
334+
assert_snapshot!(response.status(), @"200 OK");
335+
336+
Ok(())
337+
}

0 commit comments

Comments
 (0)