Skip to content

Commit e8479b5

Browse files
authored
Merge pull request #11113 from Turbo87/trustpub-create-config
Implement `PUT /api/v1/trusted_publishing/github_configs` API endpoint
2 parents a492e2d + ea00c1c commit e8479b5

25 files changed

+1059
-0
lines changed

Cargo.lock

+11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ crates_io_pagerduty = { path = "crates/crates_io_pagerduty" }
7474
crates_io_session = { path = "crates/crates_io_session" }
7575
crates_io_tarball = { path = "crates/crates_io_tarball" }
7676
crates_io_team_repo = { path = "crates/crates_io_team_repo" }
77+
crates_io_trustpub = { path = "crates/crates_io_trustpub" }
7778
crates_io_worker = { path = "crates/crates_io_worker" }
7879
csv = "=1.3.1"
7980
chrono = { version = "=0.4.41", default-features = false, features = ["serde"] }

crates/crates_io_database/src/models/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,6 @@ pub mod krate;
3333
mod owner;
3434
pub mod team;
3535
pub mod token;
36+
pub mod trustpub;
3637
pub mod user;
3738
pub mod version;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
use crate::schema::trustpub_configs_github;
2+
use chrono::{DateTime, Utc};
3+
use diesel::prelude::*;
4+
use diesel_async::{AsyncPgConnection, RunQueryDsl};
5+
6+
#[derive(Debug, Identifiable, Queryable, Selectable)]
7+
#[diesel(table_name = trustpub_configs_github, check_for_backend(diesel::pg::Pg))]
8+
pub struct GitHubConfig {
9+
pub id: i32,
10+
pub created_at: DateTime<Utc>,
11+
pub crate_id: i32,
12+
pub repository_owner: String,
13+
pub repository_owner_id: i32,
14+
pub repository_name: String,
15+
pub workflow_filename: String,
16+
pub environment: Option<String>,
17+
}
18+
19+
#[derive(Debug, Insertable)]
20+
#[diesel(table_name = trustpub_configs_github, check_for_backend(diesel::pg::Pg))]
21+
pub struct NewGitHubConfig<'a> {
22+
pub crate_id: i32,
23+
pub repository_owner: &'a str,
24+
pub repository_owner_id: i32,
25+
pub repository_name: &'a str,
26+
pub workflow_filename: &'a str,
27+
pub environment: Option<&'a str>,
28+
}
29+
30+
impl NewGitHubConfig<'_> {
31+
pub async fn insert(&self, conn: &mut AsyncPgConnection) -> QueryResult<GitHubConfig> {
32+
self.insert_into(trustpub_configs_github::table)
33+
.returning(GitHubConfig::as_returning())
34+
.get_result(conn)
35+
.await
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
mod github_config;
2+
3+
pub use self::github_config::{GitHubConfig, NewGitHubConfig};

crates/crates_io_github/examples/test_github_client.rs

+10
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ enum Request {
1212
#[clap(long, env = "GITHUB_ACCESS_TOKEN", hide_env_values = true)]
1313
access_token: SecretString,
1414
},
15+
GetUser {
16+
name: String,
17+
#[clap(long, env = "GITHUB_ACCESS_TOKEN", hide_env_values = true)]
18+
access_token: SecretString,
19+
},
1520
OrgByName {
1621
org_name: String,
1722
#[clap(long, env = "GITHUB_ACCESS_TOKEN", hide_env_values = true)]
@@ -58,6 +63,11 @@ async fn main() -> Result<()> {
5863
let response = github_client.current_user(&access_token).await?;
5964
println!("{response:#?}");
6065
}
66+
Request::GetUser { name, access_token } => {
67+
let access_token = AccessToken::new(access_token.expose_secret().into());
68+
let response = github_client.get_user(&name, &access_token).await?;
69+
println!("{response:#?}");
70+
}
6171
Request::OrgByName {
6272
org_name,
6373
access_token,

crates/crates_io_github/src/lib.rs

+6
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type Result<T> = std::result::Result<T, GitHubError>;
2020
#[async_trait]
2121
pub trait GitHubClient: Send + Sync {
2222
async fn current_user(&self, auth: &AccessToken) -> Result<GitHubUser>;
23+
async fn get_user(&self, name: &str, auth: &AccessToken) -> Result<GitHubUser>;
2324
async fn org_by_name(&self, org_name: &str, auth: &AccessToken) -> Result<GitHubOrganization>;
2425
async fn team_by_name(
2526
&self,
@@ -102,6 +103,11 @@ impl GitHubClient for RealGitHubClient {
102103
self.request("/user", auth).await
103104
}
104105

106+
async fn get_user(&self, name: &str, auth: &AccessToken) -> Result<GitHubUser> {
107+
let url = format!("/users/{name}");
108+
self.request(&url, auth).await
109+
}
110+
105111
async fn org_by_name(&self, org_name: &str, auth: &AccessToken) -> Result<GitHubOrganization> {
106112
let url = format!("/orgs/{org_name}");
107113
self.request(&url, auth).await

crates/crates_io_trustpub/Cargo.toml

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[package]
2+
name = "crates_io_trustpub"
3+
version = "0.0.0"
4+
license = "MIT OR Apache-2.0"
5+
edition = "2024"
6+
7+
[lints]
8+
workspace = true
9+
10+
[dependencies]
11+
regex = "=1.11.1"
12+
thiserror = "=2.0.12"
13+
14+
[dev-dependencies]
15+
claims = "=0.8.0"
16+
insta = "=1.43.1"

crates/crates_io_trustpub/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# crates_io_trustpub
2+
3+
This crate contains code related to the "[Trusted Publishing](https://github.com/rust-lang/rfcs/pull/3691)" feature of crates.io.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod validation;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
use std::sync::LazyLock;
2+
3+
const MAX_FIELD_LENGTH: usize = 255;
4+
5+
#[derive(Debug, thiserror::Error)]
6+
pub enum ValidationError {
7+
#[error("GitHub repository owner name may not be empty")]
8+
OwnerEmpty,
9+
#[error("GitHub repository owner name is too long (maximum is {MAX_FIELD_LENGTH} characters)")]
10+
OwnerTooLong,
11+
#[error("Invalid GitHub repository owner name")]
12+
OwnerInvalid,
13+
14+
#[error("GitHub repository name may not be empty")]
15+
RepoEmpty,
16+
#[error("GitHub repository name is too long (maximum is {MAX_FIELD_LENGTH} characters)")]
17+
RepoTooLong,
18+
#[error("Invalid GitHub repository name")]
19+
RepoInvalid,
20+
21+
#[error("Workflow filename may not be empty")]
22+
WorkflowFilenameEmpty,
23+
#[error("Workflow filename is too long (maximum is {MAX_FIELD_LENGTH} characters)")]
24+
WorkflowFilenameTooLong,
25+
#[error("Workflow filename must end with `.yml` or `.yaml`")]
26+
WorkflowFilenameMissingSuffix,
27+
#[error("Workflow filename must be a filename only, without directories")]
28+
WorkflowFilenameContainsSlash,
29+
30+
#[error("Environment name may not be empty (use `null` to omit)")]
31+
EnvironmentEmptyString,
32+
#[error("Environment name is too long (maximum is {MAX_FIELD_LENGTH} characters)")]
33+
EnvironmentTooLong,
34+
#[error("Environment name may not start with whitespace")]
35+
EnvironmentStartsWithWhitespace,
36+
#[error("Environment name may not end with whitespace")]
37+
EnvironmentEndsWithWhitespace,
38+
#[error(r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#)]
39+
EnvironmentInvalidChars,
40+
}
41+
42+
pub fn validate_owner(owner: &str) -> Result<(), ValidationError> {
43+
static RE_VALID_GITHUB_OWNER: LazyLock<regex::Regex> =
44+
LazyLock::new(|| regex::Regex::new(r"^[a-zA-Z0-9][a-zA-Z0-9-]*$").unwrap());
45+
46+
if owner.is_empty() {
47+
Err(ValidationError::OwnerEmpty)
48+
} else if owner.len() > MAX_FIELD_LENGTH {
49+
Err(ValidationError::OwnerTooLong)
50+
} else if !RE_VALID_GITHUB_OWNER.is_match(owner) {
51+
Err(ValidationError::OwnerInvalid)
52+
} else {
53+
Ok(())
54+
}
55+
}
56+
57+
pub fn validate_repo(repo: &str) -> Result<(), ValidationError> {
58+
static RE_VALID_GITHUB_REPO: LazyLock<regex::Regex> =
59+
LazyLock::new(|| regex::Regex::new(r"^[a-zA-Z0-9-_.]+$").unwrap());
60+
61+
if repo.is_empty() {
62+
Err(ValidationError::RepoEmpty)
63+
} else if repo.len() > MAX_FIELD_LENGTH {
64+
Err(ValidationError::RepoTooLong)
65+
} else if !RE_VALID_GITHUB_REPO.is_match(repo) {
66+
Err(ValidationError::RepoInvalid)
67+
} else {
68+
Ok(())
69+
}
70+
}
71+
72+
pub fn validate_workflow_filename(filename: &str) -> Result<(), ValidationError> {
73+
if filename.is_empty() {
74+
Err(ValidationError::WorkflowFilenameEmpty)
75+
} else if filename.len() > MAX_FIELD_LENGTH {
76+
Err(ValidationError::WorkflowFilenameTooLong)
77+
} else if !filename.ends_with(".yml") && !filename.ends_with(".yaml") {
78+
Err(ValidationError::WorkflowFilenameMissingSuffix)
79+
} else if filename.contains('/') {
80+
Err(ValidationError::WorkflowFilenameContainsSlash)
81+
} else {
82+
Ok(())
83+
}
84+
}
85+
86+
pub fn validate_environment(env: &str) -> Result<(), ValidationError> {
87+
static RE_INVALID_ENVIRONMENT_CHARS: LazyLock<regex::Regex> =
88+
LazyLock::new(|| regex::Regex::new(r#"[\x00-\x1F\x7F'"`,;\\]"#).unwrap());
89+
90+
if env.is_empty() {
91+
Err(ValidationError::EnvironmentEmptyString)
92+
} else if env.len() > MAX_FIELD_LENGTH {
93+
Err(ValidationError::EnvironmentTooLong)
94+
} else if env.starts_with(" ") {
95+
Err(ValidationError::EnvironmentStartsWithWhitespace)
96+
} else if env.ends_with(" ") {
97+
Err(ValidationError::EnvironmentEndsWithWhitespace)
98+
} else if RE_INVALID_ENVIRONMENT_CHARS.is_match(env) {
99+
Err(ValidationError::EnvironmentInvalidChars)
100+
} else {
101+
Ok(())
102+
}
103+
}
104+
105+
#[cfg(test)]
106+
mod tests {
107+
use super::*;
108+
use claims::assert_err;
109+
use insta::assert_snapshot;
110+
111+
#[test]
112+
fn test_validate_owner() {
113+
assert_snapshot!(assert_err!(validate_owner("")), @"GitHub repository owner name may not be empty");
114+
assert_snapshot!(assert_err!(validate_owner(&"x".repeat(256))), @"GitHub repository owner name is too long (maximum is 255 characters)");
115+
assert_snapshot!(assert_err!(validate_owner("invalid_characters@")), @"Invalid GitHub repository owner name");
116+
}
117+
118+
#[test]
119+
fn test_validate_repo() {
120+
assert_snapshot!(assert_err!(validate_repo("")), @"GitHub repository name may not be empty");
121+
assert_snapshot!(assert_err!(validate_repo(&"x".repeat(256))), @"GitHub repository name is too long (maximum is 255 characters)");
122+
assert_snapshot!(assert_err!(validate_repo("$invalid#characters")), @"Invalid GitHub repository name");
123+
}
124+
125+
#[test]
126+
fn test_validate_workflow_filename() {
127+
assert_snapshot!(assert_err!(validate_workflow_filename("")), @"Workflow filename may not be empty");
128+
assert_snapshot!(assert_err!(validate_workflow_filename(&"x".repeat(256))), @"Workflow filename is too long (maximum is 255 characters)");
129+
assert_snapshot!(assert_err!(validate_workflow_filename("missing_suffix")), @"Workflow filename must end with `.yml` or `.yaml`");
130+
assert_snapshot!(assert_err!(validate_workflow_filename("/slash")), @"Workflow filename must end with `.yml` or `.yaml`");
131+
assert_snapshot!(assert_err!(validate_workflow_filename("/many/slashes")), @"Workflow filename must end with `.yml` or `.yaml`");
132+
assert_snapshot!(assert_err!(validate_workflow_filename("/slash.yml")), @"Workflow filename must be a filename only, without directories");
133+
}
134+
135+
#[test]
136+
fn test_validate_environment() {
137+
assert_snapshot!(assert_err!(validate_environment("")), @"Environment name may not be empty (use `null` to omit)");
138+
assert_snapshot!(assert_err!(validate_environment(&"x".repeat(256))), @"Environment name is too long (maximum is 255 characters)");
139+
assert_snapshot!(assert_err!(validate_environment(" foo")), @"Environment name may not start with whitespace");
140+
assert_snapshot!(assert_err!(validate_environment("foo ")), @"Environment name may not end with whitespace");
141+
assert_snapshot!(assert_err!(validate_environment("'")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#);
142+
assert_snapshot!(assert_err!(validate_environment("\"")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#);
143+
assert_snapshot!(assert_err!(validate_environment("`")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#);
144+
assert_snapshot!(assert_err!(validate_environment(",")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#);
145+
assert_snapshot!(assert_err!(validate_environment(";")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#);
146+
assert_snapshot!(assert_err!(validate_environment("\\")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#);
147+
assert_snapshot!(assert_err!(validate_environment("\x00")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#);
148+
assert_snapshot!(assert_err!(validate_environment("\x1f")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#);
149+
assert_snapshot!(assert_err!(validate_environment("\x7f")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#);
150+
assert_snapshot!(assert_err!(validate_environment("\t")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#);
151+
assert_snapshot!(assert_err!(validate_environment("\r")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#);
152+
assert_snapshot!(assert_err!(validate_environment("\n")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#);
153+
}
154+
}

crates/crates_io_trustpub/src/lib.rs

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#![doc = include_str!("../README.md")]
2+
3+
pub mod github;

src/controllers.rs

+1
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ pub mod site_metadata;
1313
pub mod summary;
1414
pub mod team;
1515
pub mod token;
16+
pub mod trustpub;
1617
pub mod user;
1718
pub mod version;

0 commit comments

Comments
 (0)