diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-11-28 12:40:31 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-11-28 12:40:31 +0000 |
| commit | b94262161df99966fbb8aa6861fb46603039111f (patch) | |
| tree | f0194656783d05263be2d940f4e182b1bec75070 /grasp-audit/src | |
| parent | bf51a082ad54815f108bb255cf258fcae4a9bb4f (diff) | |
allow push to ref/nostr/<event-id>
Diffstat (limited to 'grasp-audit/src')
| -rw-r--r-- | grasp-audit/src/fixtures.rs | 38 | ||||
| -rw-r--r-- | grasp-audit/src/lib.rs | 2 | ||||
| -rw-r--r-- | grasp-audit/src/specs/grasp01/push_authorization.rs | 242 |
3 files changed, 280 insertions, 2 deletions
diff --git a/grasp-audit/src/fixtures.rs b/grasp-audit/src/fixtures.rs index 8cee964..b6fbc79 100644 --- a/grasp-audit/src/fixtures.rs +++ b/grasp-audit/src/fixtures.rs | |||
| @@ -1208,6 +1208,44 @@ pub fn try_push(clone_path: &Path) -> Result<bool, String> { | |||
| 1208 | Ok(output.status.success()) | 1208 | Ok(output.status.success()) |
| 1209 | } | 1209 | } |
| 1210 | 1210 | ||
| 1211 | /// Attempt a git push to a specific ref and return success/failure | ||
| 1212 | /// | ||
| 1213 | /// This is used for testing refs/nostr/<event-id> push validation. | ||
| 1214 | /// | ||
| 1215 | /// # Arguments | ||
| 1216 | /// * `clone_path` - Path to the git repository | ||
| 1217 | /// * `ref_name` - The ref to push to (e.g., "refs/nostr/<event-id>") | ||
| 1218 | /// | ||
| 1219 | /// # Returns | ||
| 1220 | /// * `Ok(true)` - Push succeeded | ||
| 1221 | /// * `Ok(false)` - Push was rejected | ||
| 1222 | /// * `Err(String)` - Error executing git push | ||
| 1223 | /// | ||
| 1224 | /// # Example | ||
| 1225 | /// ```no_run | ||
| 1226 | /// # use grasp_audit::*; | ||
| 1227 | /// # use std::path::Path; | ||
| 1228 | /// # fn example() -> Result<(), String> { | ||
| 1229 | /// let success = try_push_to_ref(Path::new("/tmp/my-repo"), "refs/nostr/abc123")?; | ||
| 1230 | /// if success { | ||
| 1231 | /// println!("Push to refs/nostr/abc123 succeeded"); | ||
| 1232 | /// } else { | ||
| 1233 | /// println!("Push was rejected"); | ||
| 1234 | /// } | ||
| 1235 | /// # Ok(()) | ||
| 1236 | /// # } | ||
| 1237 | /// ``` | ||
| 1238 | pub fn try_push_to_ref(clone_path: &Path, ref_name: &str) -> Result<bool, String> { | ||
| 1239 | let output = Command::new("git") | ||
| 1240 | .args(["push", "origin", &format!("HEAD:{}", ref_name)]) | ||
| 1241 | .current_dir(clone_path) | ||
| 1242 | .env("GIT_TERMINAL_PROMPT", "0") | ||
| 1243 | .output() | ||
| 1244 | .map_err(|e| format!("Failed to execute git push: {}", e))?; | ||
| 1245 | |||
| 1246 | Ok(output.status.success()) | ||
| 1247 | } | ||
| 1248 | |||
| 1211 | #[cfg(test)] | 1249 | #[cfg(test)] |
| 1212 | mod tests { | 1250 | mod tests { |
| 1213 | use super::*; | 1251 | use super::*; |
diff --git a/grasp-audit/src/lib.rs b/grasp-audit/src/lib.rs index 3d11bd8..5ee93b3 100644 --- a/grasp-audit/src/lib.rs +++ b/grasp-audit/src/lib.rs | |||
| @@ -40,7 +40,7 @@ pub use client::AuditClient; | |||
| 40 | pub use fixtures::{ | 40 | pub use fixtures::{ |
| 41 | // Git operation helpers | 41 | // Git operation helpers |
| 42 | clone_repo, create_commit, create_deterministic_commit, create_deterministic_commit_with_variant, | 42 | clone_repo, create_commit, create_deterministic_commit, create_deterministic_commit_with_variant, |
| 43 | try_push, | 43 | try_push, try_push_to_ref, |
| 44 | // Verification helpers | 44 | // Verification helpers |
| 45 | send_and_verify_accepted, send_and_verify_rejected, | 45 | send_and_verify_accepted, send_and_verify_rejected, |
| 46 | // Types and constants | 46 | // Types and constants |
diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs index 0a5b1ec..0170f6e 100644 --- a/grasp-audit/src/specs/grasp01/push_authorization.rs +++ b/grasp-audit/src/specs/grasp01/push_authorization.rs | |||
| @@ -18,7 +18,7 @@ | |||
| 18 | 18 | ||
| 19 | use crate::{ | 19 | use crate::{ |
| 20 | clone_repo, create_commit, create_deterministic_commit, create_deterministic_commit_with_variant, | 20 | clone_repo, create_commit, create_deterministic_commit, create_deterministic_commit_with_variant, |
| 21 | try_push, AuditClient, CommitVariant, FixtureKind, TestContext, TestResult, | 21 | try_push, try_push_to_ref, AuditClient, CommitVariant, FixtureKind, TestContext, TestResult, |
| 22 | DETERMINISTIC_COMMIT_HASH, MAINTAINER_DETERMINISTIC_COMMIT_HASH, | 22 | DETERMINISTIC_COMMIT_HASH, MAINTAINER_DETERMINISTIC_COMMIT_HASH, |
| 23 | RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH, | 23 | RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH, |
| 24 | }; | 24 | }; |
| @@ -41,6 +41,8 @@ impl PushAuthorizationTests { | |||
| 41 | results.add(Self::test_push_rejected_wrong_commit(client, relay_domain).await); | 41 | results.add(Self::test_push_rejected_wrong_commit(client, relay_domain).await); |
| 42 | results.add(Self::test_push_authorized_by_maintainer_state_only(client, relay_domain).await); | 42 | results.add(Self::test_push_authorized_by_maintainer_state_only(client, relay_domain).await); |
| 43 | results.add(Self::test_push_authorized_by_recursive_maintainer_state(client, relay_domain).await); | 43 | results.add(Self::test_push_authorized_by_recursive_maintainer_state(client, relay_domain).await); |
| 44 | results.add(Self::test_push_to_refs_nostr_valid_event_id(client, relay_domain).await); | ||
| 45 | results.add(Self::test_push_to_refs_nostr_invalid_event_id(client, relay_domain).await); | ||
| 44 | 46 | ||
| 45 | results | 47 | results |
| 46 | } | 48 | } |
| @@ -1029,6 +1031,244 @@ impl PushAuthorizationTests { | |||
| 1029 | Err(e) => TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored").fail(&e), | 1031 | Err(e) => TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored").fail(&e), |
| 1030 | } | 1032 | } |
| 1031 | } | 1033 | } |
| 1034 | |||
| 1035 | /// Test that push to refs/nostr/<event-id> succeeds with valid EventId format | ||
| 1036 | /// | ||
| 1037 | /// GRASP-01: "MUST accept pushes via this service to `refs/nostr/<event-id>`" | ||
| 1038 | /// The event_id must parse as a valid rust-nostr EventId (64-char hex string). | ||
| 1039 | /// This does NOT require the ref to be listed in any state event - it's purely format validation. | ||
| 1040 | /// | ||
| 1041 | /// ## Fixture-First Pattern | ||
| 1042 | /// | ||
| 1043 | /// 1. **Generate**: Create repo with ValidRepo fixture (no state event needed) | ||
| 1044 | /// 2. **Send**: Clone repo, create commit, push to refs/nostr/<valid-event-id> | ||
| 1045 | /// 3. **Verify**: Push should succeed because event-id format is valid | ||
| 1046 | pub async fn test_push_to_refs_nostr_valid_event_id( | ||
| 1047 | client: &AuditClient, | ||
| 1048 | relay_domain: &str, | ||
| 1049 | ) -> TestResult { | ||
| 1050 | let test_name = "test_push_to_refs_nostr_valid_event_id"; | ||
| 1051 | |||
| 1052 | // ============================================================ | ||
| 1053 | // Step 1: GENERATE - Create repo (no state event needed for refs/nostr/) | ||
| 1054 | // ============================================================ | ||
| 1055 | let ctx = TestContext::new(client); | ||
| 1056 | |||
| 1057 | let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { | ||
| 1058 | Ok(r) => r, | ||
| 1059 | Err(e) => { | ||
| 1060 | return TestResult::new( | ||
| 1061 | test_name, | ||
| 1062 | "GRASP-01", | ||
| 1063 | "Push to refs/nostr/<valid-event-id> accepted", | ||
| 1064 | ) | ||
| 1065 | .fail(&format!("Failed to create repo: {}", e)); | ||
| 1066 | } | ||
| 1067 | }; | ||
| 1068 | |||
| 1069 | tokio::time::sleep(std::time::Duration::from_millis(200)).await; | ||
| 1070 | |||
| 1071 | let repo_id = repo | ||
| 1072 | .tags | ||
| 1073 | .iter() | ||
| 1074 | .find(|t| t.kind() == TagKind::d()) | ||
| 1075 | .and_then(|t| t.content()) | ||
| 1076 | .unwrap() | ||
| 1077 | .to_string(); | ||
| 1078 | let npub = repo.pubkey.to_bech32().unwrap(); | ||
| 1079 | |||
| 1080 | // ============================================================ | ||
| 1081 | // Step 2: SEND - Clone repo, create commit, push to refs/nostr/<event-id> | ||
| 1082 | // ============================================================ | ||
| 1083 | let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { | ||
| 1084 | Ok(p) => p, | ||
| 1085 | Err(e) => { | ||
| 1086 | return TestResult::new( | ||
| 1087 | test_name, | ||
| 1088 | "GRASP-01", | ||
| 1089 | "Push to refs/nostr/<valid-event-id> accepted", | ||
| 1090 | ) | ||
| 1091 | .fail(&e); | ||
| 1092 | } | ||
| 1093 | }; | ||
| 1094 | let cleanup = || { | ||
| 1095 | let _ = fs::remove_dir_all(&clone_path); | ||
| 1096 | }; | ||
| 1097 | |||
| 1098 | // Create a unique commit | ||
| 1099 | if let Err(e) = create_commit(&clone_path, "Test commit for refs/nostr push") { | ||
| 1100 | cleanup(); | ||
| 1101 | return TestResult::new( | ||
| 1102 | test_name, | ||
| 1103 | "GRASP-01", | ||
| 1104 | "Push to refs/nostr/<valid-event-id> accepted", | ||
| 1105 | ) | ||
| 1106 | .fail(&e); | ||
| 1107 | } | ||
| 1108 | |||
| 1109 | // Generate a random event to get a valid EventId | ||
| 1110 | let keys = Keys::generate(); | ||
| 1111 | let event = match EventBuilder::text_note("test") | ||
| 1112 | .sign(&keys) | ||
| 1113 | .await | ||
| 1114 | { | ||
| 1115 | Ok(e) => e, | ||
| 1116 | Err(e) => { | ||
| 1117 | cleanup(); | ||
| 1118 | return TestResult::new( | ||
| 1119 | test_name, | ||
| 1120 | "GRASP-01", | ||
| 1121 | "Push to refs/nostr/<valid-event-id> accepted", | ||
| 1122 | ) | ||
| 1123 | .fail(&format!("Failed to create test event: {}", e)); | ||
| 1124 | } | ||
| 1125 | }; | ||
| 1126 | |||
| 1127 | // Use the event id as the refs/nostr/ target | ||
| 1128 | let ref_name = format!("refs/nostr/{}", event.id); | ||
| 1129 | |||
| 1130 | // ============================================================ | ||
| 1131 | // Step 3: VERIFY - Push should succeed with valid event-id format | ||
| 1132 | // ============================================================ | ||
| 1133 | let push_result = try_push_to_ref(&clone_path, &ref_name); | ||
| 1134 | cleanup(); | ||
| 1135 | |||
| 1136 | match push_result { | ||
| 1137 | Ok(true) => TestResult::new( | ||
| 1138 | test_name, | ||
| 1139 | "GRASP-01", | ||
| 1140 | "Push to refs/nostr/<valid-event-id> accepted", | ||
| 1141 | ) | ||
| 1142 | .pass(), | ||
| 1143 | Ok(false) => TestResult::new( | ||
| 1144 | test_name, | ||
| 1145 | "GRASP-01", | ||
| 1146 | "Push to refs/nostr/<valid-event-id> accepted", | ||
| 1147 | ) | ||
| 1148 | .fail(&format!( | ||
| 1149 | "Push to {} was rejected but should be accepted. \ | ||
| 1150 | The event-id '{}' is a valid 64-character hex string (EventId format).", | ||
| 1151 | ref_name, event.id | ||
| 1152 | )), | ||
| 1153 | Err(e) => TestResult::new( | ||
| 1154 | test_name, | ||
| 1155 | "GRASP-01", | ||
| 1156 | "Push to refs/nostr/<valid-event-id> accepted", | ||
| 1157 | ) | ||
| 1158 | .fail(&format!("Push error: {}", e)), | ||
| 1159 | } | ||
| 1160 | } | ||
| 1161 | |||
| 1162 | /// Test that push to refs/nostr/<invalid> is rejected with invalid EventId format | ||
| 1163 | /// | ||
| 1164 | /// GRASP-01: "MUST accept pushes via this service to `refs/nostr/<event-id>`" | ||
| 1165 | /// The event_id must parse as a valid rust-nostr EventId (64-char hex string). | ||
| 1166 | /// Invalid formats (too short, non-hex, etc.) should be rejected. | ||
| 1167 | /// | ||
| 1168 | /// ## Fixture-First Pattern | ||
| 1169 | /// | ||
| 1170 | /// 1. **Generate**: Create repo with ValidRepo fixture (no state event needed) | ||
| 1171 | /// 2. **Send**: Clone repo, create commit, try to push to refs/nostr/123 (invalid) | ||
| 1172 | /// 3. **Verify**: Push should be rejected because event-id format is invalid | ||
| 1173 | pub async fn test_push_to_refs_nostr_invalid_event_id( | ||
| 1174 | client: &AuditClient, | ||
| 1175 | relay_domain: &str, | ||
| 1176 | ) -> TestResult { | ||
| 1177 | let test_name = "test_push_to_refs_nostr_invalid_event_id"; | ||
| 1178 | |||
| 1179 | // ============================================================ | ||
| 1180 | // Step 1: GENERATE - Create repo (no state event needed for refs/nostr/) | ||
| 1181 | // ============================================================ | ||
| 1182 | let ctx = TestContext::new(client); | ||
| 1183 | |||
| 1184 | let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { | ||
| 1185 | Ok(r) => r, | ||
| 1186 | Err(e) => { | ||
| 1187 | return TestResult::new( | ||
| 1188 | test_name, | ||
| 1189 | "GRASP-01", | ||
| 1190 | "Push to refs/nostr/<invalid-event-id> rejected", | ||
| 1191 | ) | ||
| 1192 | .fail(&format!("Failed to create repo: {}", e)); | ||
| 1193 | } | ||
| 1194 | }; | ||
| 1195 | |||
| 1196 | tokio::time::sleep(std::time::Duration::from_millis(200)).await; | ||
| 1197 | |||
| 1198 | let repo_id = repo | ||
| 1199 | .tags | ||
| 1200 | .iter() | ||
| 1201 | .find(|t| t.kind() == TagKind::d()) | ||
| 1202 | .and_then(|t| t.content()) | ||
| 1203 | .unwrap() | ||
| 1204 | .to_string(); | ||
| 1205 | let npub = repo.pubkey.to_bech32().unwrap(); | ||
| 1206 | |||
| 1207 | // ============================================================ | ||
| 1208 | // Step 2: SEND - Clone repo, create commit, try push to invalid ref | ||
| 1209 | // ============================================================ | ||
| 1210 | let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { | ||
| 1211 | Ok(p) => p, | ||
| 1212 | Err(e) => { | ||
| 1213 | return TestResult::new( | ||
| 1214 | test_name, | ||
| 1215 | "GRASP-01", | ||
| 1216 | "Push to refs/nostr/<invalid-event-id> rejected", | ||
| 1217 | ) | ||
| 1218 | .fail(&e); | ||
| 1219 | } | ||
| 1220 | }; | ||
| 1221 | let cleanup = || { | ||
| 1222 | let _ = fs::remove_dir_all(&clone_path); | ||
| 1223 | }; | ||
| 1224 | |||
| 1225 | // Create a unique commit | ||
| 1226 | if let Err(e) = create_commit(&clone_path, "Test commit for invalid refs/nostr push") { | ||
| 1227 | cleanup(); | ||
| 1228 | return TestResult::new( | ||
| 1229 | test_name, | ||
| 1230 | "GRASP-01", | ||
| 1231 | "Push to refs/nostr/<invalid-event-id> rejected", | ||
| 1232 | ) | ||
| 1233 | .fail(&e); | ||
| 1234 | } | ||
| 1235 | |||
| 1236 | // Use an invalid event-id (too short, not a valid 64-char hex) | ||
| 1237 | let invalid_event_id = "123"; | ||
| 1238 | let ref_name = format!("refs/nostr/{}", invalid_event_id); | ||
| 1239 | |||
| 1240 | // ============================================================ | ||
| 1241 | // Step 3: VERIFY - Push should be rejected with invalid event-id format | ||
| 1242 | // ============================================================ | ||
| 1243 | let push_result = try_push_to_ref(&clone_path, &ref_name); | ||
| 1244 | cleanup(); | ||
| 1245 | |||
| 1246 | match push_result { | ||
| 1247 | Ok(false) => TestResult::new( | ||
| 1248 | test_name, | ||
| 1249 | "GRASP-01", | ||
| 1250 | "Push to refs/nostr/<invalid-event-id> rejected", | ||
| 1251 | ) | ||
| 1252 | .pass(), | ||
| 1253 | Ok(true) => TestResult::new( | ||
| 1254 | test_name, | ||
| 1255 | "GRASP-01", | ||
| 1256 | "Push to refs/nostr/<invalid-event-id> rejected", | ||
| 1257 | ) | ||
| 1258 | .fail(&format!( | ||
| 1259 | "Push to {} was accepted but should be rejected. \ | ||
| 1260 | The event-id '{}' is NOT a valid 64-character hex string (EventId format). \ | ||
| 1261 | The relay should reject pushes to refs/nostr/ with invalid event-id format.", | ||
| 1262 | ref_name, invalid_event_id | ||
| 1263 | )), | ||
| 1264 | Err(e) => TestResult::new( | ||
| 1265 | test_name, | ||
| 1266 | "GRASP-01", | ||
| 1267 | "Push to refs/nostr/<invalid-event-id> rejected", | ||
| 1268 | ) | ||
| 1269 | .fail(&format!("Push error: {}", e)), | ||
| 1270 | } | ||
| 1271 | } | ||
| 1032 | } | 1272 | } |
| 1033 | 1273 | ||
| 1034 | #[cfg(test)] | 1274 | #[cfg(test)] |