diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-01 21:48:24 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-01 21:48:24 +0000 |
| commit | 883774c7785c57bded21ba3be63d0fdac8a51dcf (patch) | |
| tree | fd37121307bd8719107e936cb3bc507499dd7ba3 /grasp-audit/src | |
| parent | 3c398b5e528f79231fa55f91225f9e79be1d43f5 (diff) | |
better fixtures: RecursiveMaintainerStateDataPushed and test_non_maintainer_state_rejected
Diffstat (limited to 'grasp-audit/src')
| -rw-r--r-- | grasp-audit/src/specs/grasp01/push_authorization.rs | 225 |
1 files changed, 54 insertions, 171 deletions
diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs index 2e14953..36abd30 100644 --- a/grasp-audit/src/specs/grasp01/push_authorization.rs +++ b/grasp-audit/src/specs/grasp01/push_authorization.rs | |||
| @@ -781,7 +781,7 @@ impl PushAuthorizationTests { | |||
| 781 | /// 4. **DataPushed**: Clones repo, creates maintainer deterministic commit, pushes to relay | 781 | /// 4. **DataPushed**: Clones repo, creates maintainer deterministic commit, pushes to relay |
| 782 | /// | 782 | /// |
| 783 | /// The test wraps the fixture result in pass/fail using the error message. | 783 | /// The test wraps the fixture result in pass/fail using the error message. |
| 784 | #[allow(unused_variables)] // relay_domain is now handled by fixture | 784 | #[allow(unused_variables)] // relay_domain is now handled by fixture |
| 785 | pub async fn test_push_authorized_by_maintainer_state_only( | 785 | pub async fn test_push_authorized_by_maintainer_state_only( |
| 786 | client: &AuditClient, | 786 | client: &AuditClient, |
| 787 | relay_domain: &str, | 787 | relay_domain: &str, |
| @@ -791,23 +791,22 @@ impl PushAuthorizationTests { | |||
| 791 | 791 | ||
| 792 | // The MaintainerStateDataPushed fixture handles all stages: | 792 | // The MaintainerStateDataPushed fixture handles all stages: |
| 793 | // Generate → Send → Verify → DataPush | 793 | // Generate → Send → Verify → DataPush |
| 794 | match ctx.get_fixture(FixtureKind::MaintainerStateDataPushed).await { | 794 | match ctx |
| 795 | Ok(_maintainer_state_event) => { | 795 | .get_fixture(FixtureKind::MaintainerStateDataPushed) |
| 796 | TestResult::new( | 796 | .await |
| 797 | test_name, | 797 | { |
| 798 | "GRASP-01", | 798 | Ok(_maintainer_state_event) => TestResult::new( |
| 799 | "Push authorized by maintainer state event only (no announcement)", | 799 | test_name, |
| 800 | ) | 800 | "GRASP-01", |
| 801 | .pass() | 801 | "Push authorized by maintainer state event only (no announcement)", |
| 802 | } | 802 | ) |
| 803 | Err(e) => { | 803 | .pass(), |
| 804 | TestResult::new( | 804 | Err(e) => TestResult::new( |
| 805 | test_name, | 805 | test_name, |
| 806 | "GRASP-01", | 806 | "GRASP-01", |
| 807 | "Push authorized by maintainer state event only (no announcement)", | 807 | "Push authorized by maintainer state event only (no announcement)", |
| 808 | ) | 808 | ) |
| 809 | .fail(format!("{}", e)) | 809 | .fail(format!("{}", e)), |
| 810 | } | ||
| 811 | } | 810 | } |
| 812 | } | 811 | } |
| 813 | 812 | ||
| @@ -825,7 +824,7 @@ impl PushAuthorizationTests { | |||
| 825 | /// 4. **DataPushed**: Clones repo, creates recursive maintainer deterministic commit, pushes to relay | 824 | /// 4. **DataPushed**: Clones repo, creates recursive maintainer deterministic commit, pushes to relay |
| 826 | /// | 825 | /// |
| 827 | /// The test wraps the fixture result in pass/fail using the error message. | 826 | /// The test wraps the fixture result in pass/fail using the error message. |
| 828 | #[allow(unused_variables)] // relay_domain is now handled by fixture | 827 | #[allow(unused_variables)] // relay_domain is now handled by fixture |
| 829 | pub async fn test_push_authorized_by_recursive_maintainer_state( | 828 | pub async fn test_push_authorized_by_recursive_maintainer_state( |
| 830 | client: &AuditClient, | 829 | client: &AuditClient, |
| 831 | relay_domain: &str, | 830 | relay_domain: &str, |
| @@ -835,23 +834,22 @@ impl PushAuthorizationTests { | |||
| 835 | 834 | ||
| 836 | // The RecursiveMaintainerStateDataPushed fixture handles all stages: | 835 | // The RecursiveMaintainerStateDataPushed fixture handles all stages: |
| 837 | // Generate → Send → Verify → DataPush | 836 | // Generate → Send → Verify → DataPush |
| 838 | match ctx.get_fixture(FixtureKind::RecursiveMaintainerStateDataPushed).await { | 837 | match ctx |
| 839 | Ok(_recursive_maintainer_state_event) => { | 838 | .get_fixture(FixtureKind::RecursiveMaintainerStateDataPushed) |
| 840 | TestResult::new( | 839 | .await |
| 841 | test_name, | 840 | { |
| 842 | "GRASP-01", | 841 | Ok(_recursive_maintainer_state_event) => TestResult::new( |
| 843 | "Push authorized by recursive maintainer state event", | 842 | test_name, |
| 844 | ) | 843 | "GRASP-01", |
| 845 | .pass() | 844 | "Push authorized by recursive maintainer state event", |
| 846 | } | 845 | ) |
| 847 | Err(e) => { | 846 | .pass(), |
| 848 | TestResult::new( | 847 | Err(e) => TestResult::new( |
| 849 | test_name, | 848 | test_name, |
| 850 | "GRASP-01", | 849 | "GRASP-01", |
| 851 | "Push authorized by recursive maintainer state event", | 850 | "Push authorized by recursive maintainer state event", |
| 852 | ) | 851 | ) |
| 853 | .fail(format!("{}", e)) | 852 | .fail(format!("{}", e)), |
| 854 | } | ||
| 855 | } | 853 | } |
| 856 | } | 854 | } |
| 857 | 855 | ||
| @@ -860,28 +858,34 @@ impl PushAuthorizationTests { | |||
| 860 | /// GRASP-01: "respecting the recursive maintainer set" | 858 | /// GRASP-01: "respecting the recursive maintainer set" |
| 861 | /// (Conversely, state events from non-maintainers MUST be ignored) | 859 | /// (Conversely, state events from non-maintainers MUST be ignored) |
| 862 | /// | 860 | /// |
| 863 | /// ## Fixture-First Pattern | 861 | /// ## Fixture Compatibility |
| 862 | /// | ||
| 863 | /// This test is compatible with any descendant of `OwnerStateDataPushed`: | ||
| 864 | /// - `OwnerStateDataPushed` - owner's state event with git data pushed | ||
| 865 | /// - `MaintainerStateDataPushed` - maintainer's state event with git data pushed | ||
| 866 | /// - `RecursiveMaintainerStateDataPushed` - recursive maintainer's state event with git data pushed | ||
| 867 | /// | ||
| 868 | /// All of these establish valid state on the relay that a non-maintainer should NOT be able to override. | ||
| 864 | /// | 869 | /// |
| 865 | /// 1. **Generate**: Create TestContext and get RepoState fixture | 870 | /// ## Test Flow |
| 866 | /// (repo announcement + state event pointing to deterministic commit) | 871 | /// |
| 867 | /// 2. **Send**: Clone repo, create deterministic commit, push (establishes state on relay) | 872 | /// 1. **Setup**: Get OwnerStateDataPushed fixture (repo + state event + git data pushed) |
| 868 | /// 3. **Attack**: Create a rogue state event signed by a non-maintainer | 873 | /// 2. **Clone**: Fresh clone of the repository |
| 869 | /// 4. **Test**: Create a new commit and try to push | 874 | /// 3. **Attack**: Create a new commit and a rogue state event signed by a non-maintainer |
| 870 | /// 5. **Verify**: Push should be rejected because rogue state event is ignored | 875 | /// 4. **Verify**: Push should be rejected because rogue state event is ignored |
| 871 | pub async fn test_non_maintainer_state_rejected( | 876 | pub async fn test_non_maintainer_state_rejected( |
| 872 | client: &AuditClient, | 877 | client: &AuditClient, |
| 873 | relay_domain: &str, | 878 | relay_domain: &str, |
| 874 | ) -> TestResult { | 879 | ) -> TestResult { |
| 875 | use std::process::Command; | ||
| 876 | |||
| 877 | let test_name = "test_non_maintainer_state_rejected"; | 880 | let test_name = "test_non_maintainer_state_rejected"; |
| 878 | 881 | ||
| 879 | // ============================================================ | 882 | // ============================================================ |
| 880 | // Step 1: GENERATE - Create TestContext and get RepoState fixture | 883 | // Step 1: SETUP - Get OwnerStateDataPushed fixture |
| 884 | // This establishes valid state on the relay with git data | ||
| 881 | // ============================================================ | 885 | // ============================================================ |
| 882 | let ctx = TestContext::new(client); | 886 | let ctx = TestContext::new(client); |
| 883 | 887 | ||
| 884 | let state_event = match ctx.get_fixture(FixtureKind::RepoState).await { | 888 | let state_event = match ctx.get_fixture(FixtureKind::OwnerStateDataPushed).await { |
| 885 | Ok(e) => e, | 889 | Ok(e) => e, |
| 886 | Err(e) => { | 890 | Err(e) => { |
| 887 | return TestResult::new( | 891 | return TestResult::new( |
| @@ -889,12 +893,10 @@ impl PushAuthorizationTests { | |||
| 889 | "GRASP-01", | 893 | "GRASP-01", |
| 890 | "Non-maintainer state events ignored", | 894 | "Non-maintainer state events ignored", |
| 891 | ) | 895 | ) |
| 892 | .fail(format!("Failed to create RepoState fixture: {}", e)); | 896 | .fail(format!("Failed to get OwnerStateDataPushed fixture: {}", e)); |
| 893 | } | 897 | } |
| 894 | }; | 898 | }; |
| 895 | 899 | ||
| 896 | tokio::time::sleep(std::time::Duration::from_millis(200)).await; | ||
| 897 | |||
| 898 | // Extract repo_id and npub from state event | 900 | // Extract repo_id and npub from state event |
| 899 | let repo_id = match state_event | 901 | let repo_id = match state_event |
| 900 | .tags | 902 | .tags |
| @@ -926,8 +928,7 @@ impl PushAuthorizationTests { | |||
| 926 | }; | 928 | }; |
| 927 | 929 | ||
| 928 | // ============================================================ | 930 | // ============================================================ |
| 929 | // Step 2: SEND - Clone repo, create deterministic commit, push | 931 | // Step 2: CLONE - Fresh clone of the repository |
| 930 | // (establishes the state on the relay) | ||
| 931 | // ============================================================ | 932 | // ============================================================ |
| 932 | let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { | 933 | let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { |
| 933 | Ok(p) => p, | 934 | Ok(p) => p, |
| @@ -946,124 +947,6 @@ impl PushAuthorizationTests { | |||
| 946 | let _ = fs::remove_dir_all(&clone_path); | 947 | let _ = fs::remove_dir_all(&clone_path); |
| 947 | }; | 948 | }; |
| 948 | 949 | ||
| 949 | // Create deterministic commit locally | ||
| 950 | let commit_hash = match create_deterministic_commit(&clone_path, "Initial commit") { | ||
| 951 | Ok(h) => h, | ||
| 952 | Err(e) => { | ||
| 953 | cleanup(); | ||
| 954 | return TestResult::new( | ||
| 955 | test_name, | ||
| 956 | "GRASP-01", | ||
| 957 | "Non-maintainer state events ignored", | ||
| 958 | ) | ||
| 959 | .fail(format!("Failed to create deterministic commit: {}", e)); | ||
| 960 | } | ||
| 961 | }; | ||
| 962 | |||
| 963 | // Verify commit hash matches expected | ||
| 964 | if commit_hash != DETERMINISTIC_COMMIT_HASH { | ||
| 965 | cleanup(); | ||
| 966 | return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") | ||
| 967 | .fail(format!( | ||
| 968 | "Commit hash mismatch: got {}, expected {}", | ||
| 969 | commit_hash, DETERMINISTIC_COMMIT_HASH | ||
| 970 | )); | ||
| 971 | } | ||
| 972 | |||
| 973 | // Create main branch pointing to our deterministic commit | ||
| 974 | let branch_output = Command::new("git") | ||
| 975 | .args(["branch", "main"]) | ||
| 976 | .current_dir(&clone_path) | ||
| 977 | .output(); | ||
| 978 | |||
| 979 | match branch_output { | ||
| 980 | Err(e) => { | ||
| 981 | cleanup(); | ||
| 982 | return TestResult::new( | ||
| 983 | test_name, | ||
| 984 | "GRASP-01", | ||
| 985 | "Non-maintainer state events ignored", | ||
| 986 | ) | ||
| 987 | .fail(format!("Failed to create main branch: {}", e)); | ||
| 988 | } | ||
| 989 | Ok(output) if !output.status.success() => { | ||
| 990 | cleanup(); | ||
| 991 | return TestResult::new( | ||
| 992 | test_name, | ||
| 993 | "GRASP-01", | ||
| 994 | "Non-maintainer state events ignored", | ||
| 995 | ) | ||
| 996 | .fail(format!( | ||
| 997 | "Failed to create main branch: {}", | ||
| 998 | String::from_utf8_lossy(&output.stderr) | ||
| 999 | )); | ||
| 1000 | } | ||
| 1001 | _ => {} | ||
| 1002 | } | ||
| 1003 | |||
| 1004 | // Checkout main branch | ||
| 1005 | let checkout_output = Command::new("git") | ||
| 1006 | .args(["checkout", "main"]) | ||
| 1007 | .current_dir(&clone_path) | ||
| 1008 | .output(); | ||
| 1009 | |||
| 1010 | match checkout_output { | ||
| 1011 | Err(e) => { | ||
| 1012 | cleanup(); | ||
| 1013 | return TestResult::new( | ||
| 1014 | test_name, | ||
| 1015 | "GRASP-01", | ||
| 1016 | "Non-maintainer state events ignored", | ||
| 1017 | ) | ||
| 1018 | .fail(format!("Failed to checkout main branch: {}", e)); | ||
| 1019 | } | ||
| 1020 | Ok(output) if !output.status.success() => { | ||
| 1021 | cleanup(); | ||
| 1022 | return TestResult::new( | ||
| 1023 | test_name, | ||
| 1024 | "GRASP-01", | ||
| 1025 | "Non-maintainer state events ignored", | ||
| 1026 | ) | ||
| 1027 | .fail(format!( | ||
| 1028 | "Failed to checkout main branch: {}", | ||
| 1029 | String::from_utf8_lossy(&output.stderr) | ||
| 1030 | )); | ||
| 1031 | } | ||
| 1032 | _ => {} | ||
| 1033 | } | ||
| 1034 | |||
| 1035 | // Push the deterministic commit to establish state on relay | ||
| 1036 | let push_output = Command::new("git") | ||
| 1037 | .args(["push", "origin", "main"]) | ||
| 1038 | .current_dir(&clone_path) | ||
| 1039 | .env("GIT_TERMINAL_PROMPT", "0") | ||
| 1040 | .output(); | ||
| 1041 | |||
| 1042 | match push_output { | ||
| 1043 | Err(e) => { | ||
| 1044 | cleanup(); | ||
| 1045 | return TestResult::new( | ||
| 1046 | test_name, | ||
| 1047 | "GRASP-01", | ||
| 1048 | "Non-maintainer state events ignored", | ||
| 1049 | ) | ||
| 1050 | .fail(format!("Failed to push initial commit: {}", e)); | ||
| 1051 | } | ||
| 1052 | Ok(output) if !output.status.success() => { | ||
| 1053 | cleanup(); | ||
| 1054 | return TestResult::new( | ||
| 1055 | test_name, | ||
| 1056 | "GRASP-01", | ||
| 1057 | "Non-maintainer state events ignored", | ||
| 1058 | ) | ||
| 1059 | .fail(format!( | ||
| 1060 | "Failed to push initial commit: {}", | ||
| 1061 | String::from_utf8_lossy(&output.stderr) | ||
| 1062 | )); | ||
| 1063 | } | ||
| 1064 | _ => {} | ||
| 1065 | } | ||
| 1066 | |||
| 1067 | // ============================================================ | 950 | // ============================================================ |
| 1068 | // Step 3: ATTACK - Create a new commit and a rogue state event | 951 | // Step 3: ATTACK - Create a new commit and a rogue state event |
| 1069 | // from a non-maintainer | 952 | // from a non-maintainer |
| @@ -1118,7 +1001,7 @@ impl PushAuthorizationTests { | |||
| 1118 | tokio::time::sleep(std::time::Duration::from_millis(200)).await; | 1001 | tokio::time::sleep(std::time::Duration::from_millis(200)).await; |
| 1119 | 1002 | ||
| 1120 | // ============================================================ | 1003 | // ============================================================ |
| 1121 | // Step 4 & 5: VERIFY - Push should be rejected because rogue | 1004 | // Step 4: VERIFY - Push should be rejected because rogue |
| 1122 | // state event is ignored | 1005 | // state event is ignored |
| 1123 | // ============================================================ | 1006 | // ============================================================ |
| 1124 | let push_result = try_push(&clone_path); | 1007 | let push_result = try_push(&clone_path); |