Skip to content

Commit e3d98b8

Browse files
committed
trustpub: Implement basic GitLab validation fns
1 parent abcadb5 commit e3d98b8

File tree

2 files changed

+200
-0
lines changed

2 files changed

+200
-0
lines changed

crates/crates_io_trustpub/src/gitlab/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
mod claims;
22
#[cfg(any(test, feature = "test-helpers"))]
33
pub mod test_helpers;
4+
pub mod validation;
45
mod workflows;
56

67
pub use self::claims::GitLabClaims;
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
//! Validation functions for GitLab Trusted Publishing configuration fields.
2+
//!
3+
//! This module performs basic validation of user input for GitLab CI/CD trusted publishing
4+
//! configurations. The validation rules are intentionally permissive: they accept all valid
5+
//! GitLab values while rejecting obviously invalid input. This approach is enough for our
6+
//! purposes since GitLab's JWT claims will only contain valid values anyway.
7+
//!
8+
//! See <https://docs.gitlab.com/user/reserved_names/#rules-for-usernames-project-and-group-names-and-slugs>
9+
//! and <https://docs.gitlab.com/ci/yaml/#environment>.
10+
11+
use std::sync::LazyLock;
12+
13+
const MAX_FIELD_LENGTH: usize = 255;
14+
15+
#[derive(Debug, thiserror::Error)]
16+
pub enum ValidationError {
17+
#[error("GitLab namespace may not be empty")]
18+
NamespaceEmpty,
19+
#[error("GitLab namespace is too long (maximum is {MAX_FIELD_LENGTH} characters)")]
20+
NamespaceTooLong,
21+
#[error("Invalid GitLab namespace")]
22+
NamespaceInvalid,
23+
#[error("GitLab namespace cannot end with .atom or .git")]
24+
NamespaceInvalidSuffix,
25+
26+
#[error("GitLab project name may not be empty")]
27+
ProjectEmpty,
28+
#[error("GitLab project name is too long (maximum is {MAX_FIELD_LENGTH} characters)")]
29+
ProjectTooLong,
30+
#[error("Invalid GitLab project name")]
31+
ProjectInvalid,
32+
#[error("GitLab project name cannot end with .atom or .git")]
33+
ProjectInvalidSuffix,
34+
35+
#[error("Workflow filepath may not be empty")]
36+
WorkflowFilepathEmpty,
37+
#[error("Workflow filepath is too long (maximum is {MAX_FIELD_LENGTH} characters)")]
38+
WorkflowFilepathTooLong,
39+
#[error("Workflow filepath must end with `.yml` or `.yaml`")]
40+
WorkflowFilepathMissingSuffix,
41+
#[error("Workflow filepath cannot start with /")]
42+
WorkflowFilepathStartsWithSlash,
43+
#[error("Workflow filepath cannot end with /")]
44+
WorkflowFilepathEndsWithSlash,
45+
46+
#[error("Environment name may not be empty (use `null` to omit)")]
47+
EnvironmentEmptyString,
48+
#[error("Environment name is too long (maximum is {MAX_FIELD_LENGTH} characters)")]
49+
EnvironmentTooLong,
50+
#[error("Environment name contains invalid characters")]
51+
EnvironmentInvalidChars,
52+
}
53+
54+
pub fn validate_namespace(namespace: &str) -> Result<(), ValidationError> {
55+
static RE_VALID_NAMESPACE: LazyLock<regex::Regex> = LazyLock::new(|| {
56+
regex::Regex::new(r"^[a-zA-Z0-9](?:[a-zA-Z0-9_.\-/]*[a-zA-Z0-9])?$").unwrap()
57+
});
58+
59+
if namespace.is_empty() {
60+
Err(ValidationError::NamespaceEmpty)
61+
} else if namespace.len() > MAX_FIELD_LENGTH {
62+
Err(ValidationError::NamespaceTooLong)
63+
} else if namespace.ends_with(".atom") || namespace.ends_with(".git") {
64+
Err(ValidationError::NamespaceInvalidSuffix)
65+
} else if !RE_VALID_NAMESPACE.is_match(namespace) {
66+
Err(ValidationError::NamespaceInvalid)
67+
} else {
68+
Ok(())
69+
}
70+
}
71+
72+
pub fn validate_project(project: &str) -> Result<(), ValidationError> {
73+
static RE_VALID_PROJECT: LazyLock<regex::Regex> = LazyLock::new(|| {
74+
regex::Regex::new(r"^[a-zA-Z0-9](?:[a-zA-Z0-9_.\-]*[a-zA-Z0-9])?$").unwrap()
75+
});
76+
77+
if project.is_empty() {
78+
Err(ValidationError::ProjectEmpty)
79+
} else if project.len() > MAX_FIELD_LENGTH {
80+
Err(ValidationError::ProjectTooLong)
81+
} else if project.ends_with(".atom") || project.ends_with(".git") {
82+
Err(ValidationError::ProjectInvalidSuffix)
83+
} else if !RE_VALID_PROJECT.is_match(project) {
84+
Err(ValidationError::ProjectInvalid)
85+
} else {
86+
Ok(())
87+
}
88+
}
89+
90+
pub fn validate_workflow_filepath(filepath: &str) -> Result<(), ValidationError> {
91+
if filepath.is_empty() {
92+
Err(ValidationError::WorkflowFilepathEmpty)
93+
} else if filepath.len() > MAX_FIELD_LENGTH {
94+
Err(ValidationError::WorkflowFilepathTooLong)
95+
} else if filepath.starts_with('/') {
96+
Err(ValidationError::WorkflowFilepathStartsWithSlash)
97+
} else if filepath.ends_with('/') {
98+
Err(ValidationError::WorkflowFilepathEndsWithSlash)
99+
} else if !filepath.ends_with(".yml") && !filepath.ends_with(".yaml") {
100+
Err(ValidationError::WorkflowFilepathMissingSuffix)
101+
} else {
102+
Ok(())
103+
}
104+
}
105+
106+
pub fn validate_environment(env: &str) -> Result<(), ValidationError> {
107+
// see https://docs.gitlab.com/ci/yaml/#environment
108+
109+
static RE_VALID_ENVIRONMENT: LazyLock<regex::Regex> =
110+
LazyLock::new(|| regex::Regex::new(r"^[a-zA-Z0-9 \-_/${}]+$").unwrap());
111+
112+
if env.is_empty() {
113+
Err(ValidationError::EnvironmentEmptyString)
114+
} else if env.len() > MAX_FIELD_LENGTH {
115+
Err(ValidationError::EnvironmentTooLong)
116+
} else if !RE_VALID_ENVIRONMENT.is_match(env) {
117+
Err(ValidationError::EnvironmentInvalidChars)
118+
} else {
119+
Ok(())
120+
}
121+
}
122+
123+
#[cfg(test)]
124+
mod tests {
125+
use super::*;
126+
use claims::{assert_err, assert_ok};
127+
use insta::assert_snapshot;
128+
129+
#[test]
130+
fn test_validate_namespace() {
131+
assert_snapshot!(assert_err!(validate_namespace("")), @"GitLab namespace may not be empty");
132+
assert_snapshot!(assert_err!(validate_namespace(&"x".repeat(256))), @"GitLab namespace is too long (maximum is 255 characters)");
133+
assert_snapshot!(assert_err!(validate_namespace("-")), @"Invalid GitLab namespace");
134+
assert_snapshot!(assert_err!(validate_namespace("_")), @"Invalid GitLab namespace");
135+
assert_snapshot!(assert_err!(validate_namespace("-start")), @"Invalid GitLab namespace");
136+
assert_snapshot!(assert_err!(validate_namespace("end-")), @"Invalid GitLab namespace");
137+
assert_snapshot!(assert_err!(validate_namespace("invalid@chars")), @"Invalid GitLab namespace");
138+
assert_snapshot!(assert_err!(validate_namespace("foo+bar")), @"Invalid GitLab namespace");
139+
assert_snapshot!(assert_err!(validate_namespace("foo.atom")), @"GitLab namespace cannot end with .atom or .git");
140+
assert_snapshot!(assert_err!(validate_namespace("foo.git")), @"GitLab namespace cannot end with .atom or .git");
141+
142+
assert_ok!(validate_namespace("a"));
143+
assert_ok!(validate_namespace("foo"));
144+
assert_ok!(validate_namespace("foo-bar"));
145+
assert_ok!(validate_namespace("foo_bar"));
146+
assert_ok!(validate_namespace("foo.bar"));
147+
assert_ok!(validate_namespace("foo/bar"));
148+
assert_ok!(validate_namespace("foo/bar/baz"));
149+
}
150+
151+
#[test]
152+
fn test_validate_project() {
153+
assert_snapshot!(assert_err!(validate_project("")), @"GitLab project name may not be empty");
154+
assert_snapshot!(assert_err!(validate_project(&"x".repeat(256))), @"GitLab project name is too long (maximum is 255 characters)");
155+
assert_snapshot!(assert_err!(validate_project("-")), @"Invalid GitLab project name");
156+
assert_snapshot!(assert_err!(validate_project("_")), @"Invalid GitLab project name");
157+
assert_snapshot!(assert_err!(validate_project("-start")), @"Invalid GitLab project name");
158+
assert_snapshot!(assert_err!(validate_project("end-")), @"Invalid GitLab project name");
159+
assert_snapshot!(assert_err!(validate_project("invalid/chars")), @"Invalid GitLab project name");
160+
assert_snapshot!(assert_err!(validate_project("foo.atom")), @"GitLab project name cannot end with .atom or .git");
161+
assert_snapshot!(assert_err!(validate_project("foo.git")), @"GitLab project name cannot end with .atom or .git");
162+
163+
assert_ok!(validate_project("a"));
164+
assert_ok!(validate_project("foo"));
165+
assert_ok!(validate_project("foo-bar"));
166+
assert_ok!(validate_project("foo_bar"));
167+
assert_ok!(validate_project("foo.bar"));
168+
}
169+
170+
#[test]
171+
fn test_validate_workflow_filepath() {
172+
assert_snapshot!(assert_err!(validate_workflow_filepath("")), @"Workflow filepath may not be empty");
173+
assert_snapshot!(assert_err!(validate_workflow_filepath(&"x".repeat(256))), @"Workflow filepath is too long (maximum is 255 characters)");
174+
assert_snapshot!(assert_err!(validate_workflow_filepath("/starts-with-slash.yml")), @"Workflow filepath cannot start with /");
175+
assert_snapshot!(assert_err!(validate_workflow_filepath("ends-with-slash/")), @"Workflow filepath cannot end with /");
176+
assert_snapshot!(assert_err!(validate_workflow_filepath("no-suffix")), @"Workflow filepath must end with `.yml` or `.yaml`");
177+
178+
assert_ok!(validate_workflow_filepath(".gitlab-ci.yml"));
179+
assert_ok!(validate_workflow_filepath(".gitlab-ci.yaml"));
180+
assert_ok!(validate_workflow_filepath("publish.yml"));
181+
assert_ok!(validate_workflow_filepath(".gitlab/ci/publish.yml"));
182+
assert_ok!(validate_workflow_filepath("ci/publish.yaml"));
183+
}
184+
185+
#[test]
186+
fn test_validate_environment() {
187+
assert_snapshot!(assert_err!(validate_environment("")), @"Environment name may not be empty (use `null` to omit)");
188+
assert_snapshot!(assert_err!(validate_environment(&"x".repeat(256))), @"Environment name is too long (maximum is 255 characters)");
189+
assert_snapshot!(assert_err!(validate_environment("invalid@chars")), @"Environment name contains invalid characters");
190+
assert_snapshot!(assert_err!(validate_environment("invalid.dot")), @"Environment name contains invalid characters");
191+
192+
assert_ok!(validate_environment("production"));
193+
assert_ok!(validate_environment("staging"));
194+
assert_ok!(validate_environment("prod-us-east"));
195+
assert_ok!(validate_environment("env_name"));
196+
assert_ok!(validate_environment("path/to/env"));
197+
assert_ok!(validate_environment("with space"));
198+
}
199+
}

0 commit comments

Comments
 (0)