-
Notifications
You must be signed in to change notification settings - Fork 642
Implement PUT /api/v1/trusted_publishing/github_configs
API endpoint
#11113
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0a25aee
eb03952
70dfb3b
a08f44d
3cf627a
ea00c1c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,5 +33,6 @@ pub mod krate; | |
mod owner; | ||
pub mod team; | ||
pub mod token; | ||
pub mod trustpub; | ||
pub mod user; | ||
pub mod version; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
use crate::schema::trustpub_configs_github; | ||
use chrono::{DateTime, Utc}; | ||
use diesel::prelude::*; | ||
use diesel_async::{AsyncPgConnection, RunQueryDsl}; | ||
|
||
#[derive(Debug, Identifiable, Queryable, Selectable)] | ||
#[diesel(table_name = trustpub_configs_github, check_for_backend(diesel::pg::Pg))] | ||
pub struct GitHubConfig { | ||
pub id: i32, | ||
pub created_at: DateTime<Utc>, | ||
pub crate_id: i32, | ||
pub repository_owner: String, | ||
pub repository_owner_id: i32, | ||
pub repository_name: String, | ||
pub workflow_filename: String, | ||
pub environment: Option<String>, | ||
} | ||
|
||
#[derive(Debug, Insertable)] | ||
#[diesel(table_name = trustpub_configs_github, check_for_backend(diesel::pg::Pg))] | ||
pub struct NewGitHubConfig<'a> { | ||
pub crate_id: i32, | ||
pub repository_owner: &'a str, | ||
pub repository_owner_id: i32, | ||
pub repository_name: &'a str, | ||
pub workflow_filename: &'a str, | ||
pub environment: Option<&'a str>, | ||
} | ||
|
||
impl NewGitHubConfig<'_> { | ||
pub async fn insert(&self, conn: &mut AsyncPgConnection) -> QueryResult<GitHubConfig> { | ||
self.insert_into(trustpub_configs_github::table) | ||
.returning(GitHubConfig::as_returning()) | ||
.get_result(conn) | ||
.await | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
mod github_config; | ||
|
||
pub use self::github_config::{GitHubConfig, NewGitHubConfig}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
[package] | ||
name = "crates_io_trustpub" | ||
version = "0.0.0" | ||
license = "MIT OR Apache-2.0" | ||
edition = "2024" | ||
|
||
[lints] | ||
workspace = true | ||
|
||
[dependencies] | ||
regex = "=1.11.1" | ||
thiserror = "=2.0.12" | ||
|
||
[dev-dependencies] | ||
claims = "=0.8.0" | ||
insta = "=1.43.1" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# crates_io_trustpub | ||
|
||
This crate contains code related to the "[Trusted Publishing](https://github.com/rust-lang/rfcs/pull/3691)" feature of crates.io. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
pub mod validation; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
use std::sync::LazyLock; | ||
|
||
const MAX_FIELD_LENGTH: usize = 255; | ||
|
||
#[derive(Debug, thiserror::Error)] | ||
pub enum ValidationError { | ||
#[error("GitHub repository owner name may not be empty")] | ||
OwnerEmpty, | ||
#[error("GitHub repository owner name is too long (maximum is {MAX_FIELD_LENGTH} characters)")] | ||
OwnerTooLong, | ||
#[error("Invalid GitHub repository owner name")] | ||
OwnerInvalid, | ||
|
||
#[error("GitHub repository name may not be empty")] | ||
RepoEmpty, | ||
#[error("GitHub repository name is too long (maximum is {MAX_FIELD_LENGTH} characters)")] | ||
RepoTooLong, | ||
#[error("Invalid GitHub repository name")] | ||
RepoInvalid, | ||
|
||
#[error("Workflow filename may not be empty")] | ||
WorkflowFilenameEmpty, | ||
#[error("Workflow filename is too long (maximum is {MAX_FIELD_LENGTH} characters)")] | ||
WorkflowFilenameTooLong, | ||
#[error("Workflow filename must end with `.yml` or `.yaml`")] | ||
WorkflowFilenameMissingSuffix, | ||
#[error("Workflow filename must be a filename only, without directories")] | ||
WorkflowFilenameContainsSlash, | ||
|
||
#[error("Environment name may not be empty (use `null` to omit)")] | ||
EnvironmentEmptyString, | ||
#[error("Environment name is too long (maximum is {MAX_FIELD_LENGTH} characters)")] | ||
EnvironmentTooLong, | ||
#[error("Environment name may not start with whitespace")] | ||
EnvironmentStartsWithWhitespace, | ||
#[error("Environment name may not end with whitespace")] | ||
EnvironmentEndsWithWhitespace, | ||
#[error(r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#)] | ||
EnvironmentInvalidChars, | ||
} | ||
|
||
pub fn validate_owner(owner: &str) -> Result<(), ValidationError> { | ||
static RE_VALID_GITHUB_OWNER: LazyLock<regex::Regex> = | ||
LazyLock::new(|| regex::Regex::new(r"^[a-zA-Z0-9][a-zA-Z0-9-]*$").unwrap()); | ||
|
||
if owner.is_empty() { | ||
Err(ValidationError::OwnerEmpty) | ||
} else if owner.len() > MAX_FIELD_LENGTH { | ||
Err(ValidationError::OwnerTooLong) | ||
} else if !RE_VALID_GITHUB_OWNER.is_match(owner) { | ||
Err(ValidationError::OwnerInvalid) | ||
} else { | ||
Ok(()) | ||
} | ||
} | ||
|
||
pub fn validate_repo(repo: &str) -> Result<(), ValidationError> { | ||
static RE_VALID_GITHUB_REPO: LazyLock<regex::Regex> = | ||
LazyLock::new(|| regex::Regex::new(r"^[a-zA-Z0-9-_.]+$").unwrap()); | ||
|
||
if repo.is_empty() { | ||
Err(ValidationError::RepoEmpty) | ||
} else if repo.len() > MAX_FIELD_LENGTH { | ||
Err(ValidationError::RepoTooLong) | ||
} else if !RE_VALID_GITHUB_REPO.is_match(repo) { | ||
Err(ValidationError::RepoInvalid) | ||
Comment on lines
+58
to
+66
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't seem to cover the following cases:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the currently proposed regex might be sufficient, as the |
||
} else { | ||
Ok(()) | ||
} | ||
} | ||
|
||
pub fn validate_workflow_filename(filename: &str) -> Result<(), ValidationError> { | ||
if filename.is_empty() { | ||
Err(ValidationError::WorkflowFilenameEmpty) | ||
} else if filename.len() > MAX_FIELD_LENGTH { | ||
Err(ValidationError::WorkflowFilenameTooLong) | ||
} else if !filename.ends_with(".yml") && !filename.ends_with(".yaml") { | ||
Err(ValidationError::WorkflowFilenameMissingSuffix) | ||
} else if filename.contains('/') { | ||
Err(ValidationError::WorkflowFilenameContainsSlash) | ||
} else { | ||
Ok(()) | ||
} | ||
} | ||
|
||
pub fn validate_environment(env: &str) -> Result<(), ValidationError> { | ||
static RE_INVALID_ENVIRONMENT_CHARS: LazyLock<regex::Regex> = | ||
LazyLock::new(|| regex::Regex::new(r#"[\x00-\x1F\x7F'"`,;\\]"#).unwrap()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm betting you had fun figuring out what "non-printable character" meant in this context (from the GitHub error). |
||
|
||
if env.is_empty() { | ||
Err(ValidationError::EnvironmentEmptyString) | ||
} else if env.len() > MAX_FIELD_LENGTH { | ||
Err(ValidationError::EnvironmentTooLong) | ||
} else if env.starts_with(" ") { | ||
Err(ValidationError::EnvironmentStartsWithWhitespace) | ||
} else if env.ends_with(" ") { | ||
Err(ValidationError::EnvironmentEndsWithWhitespace) | ||
} else if RE_INVALID_ENVIRONMENT_CHARS.is_match(env) { | ||
Err(ValidationError::EnvironmentInvalidChars) | ||
} else { | ||
Ok(()) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
use claims::assert_err; | ||
use insta::assert_snapshot; | ||
|
||
#[test] | ||
fn test_validate_owner() { | ||
assert_snapshot!(assert_err!(validate_owner("")), @"GitHub repository owner name may not be empty"); | ||
assert_snapshot!(assert_err!(validate_owner(&"x".repeat(256))), @"GitHub repository owner name is too long (maximum is 255 characters)"); | ||
assert_snapshot!(assert_err!(validate_owner("invalid_characters@")), @"Invalid GitHub repository owner name"); | ||
} | ||
|
||
#[test] | ||
fn test_validate_repo() { | ||
assert_snapshot!(assert_err!(validate_repo("")), @"GitHub repository name may not be empty"); | ||
assert_snapshot!(assert_err!(validate_repo(&"x".repeat(256))), @"GitHub repository name is too long (maximum is 255 characters)"); | ||
assert_snapshot!(assert_err!(validate_repo("$invalid#characters")), @"Invalid GitHub repository name"); | ||
} | ||
|
||
#[test] | ||
fn test_validate_workflow_filename() { | ||
assert_snapshot!(assert_err!(validate_workflow_filename("")), @"Workflow filename may not be empty"); | ||
assert_snapshot!(assert_err!(validate_workflow_filename(&"x".repeat(256))), @"Workflow filename is too long (maximum is 255 characters)"); | ||
assert_snapshot!(assert_err!(validate_workflow_filename("missing_suffix")), @"Workflow filename must end with `.yml` or `.yaml`"); | ||
assert_snapshot!(assert_err!(validate_workflow_filename("/slash")), @"Workflow filename must end with `.yml` or `.yaml`"); | ||
assert_snapshot!(assert_err!(validate_workflow_filename("/many/slashes")), @"Workflow filename must end with `.yml` or `.yaml`"); | ||
assert_snapshot!(assert_err!(validate_workflow_filename("/slash.yml")), @"Workflow filename must be a filename only, without directories"); | ||
} | ||
|
||
#[test] | ||
fn test_validate_environment() { | ||
assert_snapshot!(assert_err!(validate_environment("")), @"Environment name may not be empty (use `null` to omit)"); | ||
assert_snapshot!(assert_err!(validate_environment(&"x".repeat(256))), @"Environment name is too long (maximum is 255 characters)"); | ||
assert_snapshot!(assert_err!(validate_environment(" foo")), @"Environment name may not start with whitespace"); | ||
assert_snapshot!(assert_err!(validate_environment("foo ")), @"Environment name may not end with whitespace"); | ||
assert_snapshot!(assert_err!(validate_environment("'")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#); | ||
assert_snapshot!(assert_err!(validate_environment("\"")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#); | ||
assert_snapshot!(assert_err!(validate_environment("`")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#); | ||
assert_snapshot!(assert_err!(validate_environment(",")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#); | ||
assert_snapshot!(assert_err!(validate_environment(";")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#); | ||
assert_snapshot!(assert_err!(validate_environment("\\")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#); | ||
assert_snapshot!(assert_err!(validate_environment("\x00")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#); | ||
assert_snapshot!(assert_err!(validate_environment("\x1f")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#); | ||
assert_snapshot!(assert_err!(validate_environment("\x7f")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#); | ||
assert_snapshot!(assert_err!(validate_environment("\t")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#); | ||
assert_snapshot!(assert_err!(validate_environment("\r")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#); | ||
assert_snapshot!(assert_err!(validate_environment("\n")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
#![doc = include_str!("../README.md")] | ||
|
||
pub mod github; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't seem to cover the following cases:
-
39