| 1 | //! User-owned approval of the exact project hooks file, separate from folder trust. |
| 2 | use sha2::{Digest, Sha256}; |
| 3 | use std::path::{Path, PathBuf}; |
| 4 | |
| 5 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 6 | pub struct ProjectHookAuthority { |
| 7 | pub workspace: PathBuf, |
| 8 | pub digest: String, |
| 9 | } |
| 10 | |
| 11 | /// Read once for both the digest and parsing. Repository symlinks and special |
| 12 | /// files are not executable configuration, even if the workspace is trusted. |
| 13 | pub(crate) fn review_project_hooks( |
| 14 | workspace: &Path, |
| 15 | ) -> Result<(ProjectHookAuthority, String), String> { |
| 16 | let workspace = workspace |
| 17 | .canonicalize() |
| 18 | .map_err(|_| "Cannot resolve hooks workspace")?; |
| 19 | let mut path = workspace.clone(); |
| 20 | for component in [".codewhale", "hooks.toml"] { |
| 21 | path.push(component); |
| 22 | let metadata = |
| 23 | std::fs::symlink_metadata(&path).map_err(|_| "Cannot read project hooks path")?; |
| 24 | if metadata.file_type().is_symlink() { |
| 25 | return Err("Project hooks path must not contain symlinks".into()); |
| 26 | } |
| 27 | if (component == ".codewhale" && !metadata.is_dir()) |
| 28 | || (component == "hooks.toml" && !metadata.is_file()) |
| 29 | { |
| 30 | return Err("Project hooks must be a regular file in .codewhale".into()); |
| 31 | } |
| 32 | } |
| 33 | let contents = super::config::read_project_hooks_file(&path) |
| 34 | .map_err(|_| "Cannot read project hooks file (maximum 1 MiB)")?; |
| 35 | let digest = Sha256::digest(contents.as_bytes()) |
| 36 | .iter() |
| 37 | .map(|byte| format!("{byte:02x}")) |
| 38 | .collect(); |
| 39 | Ok((ProjectHookAuthority { workspace, digest }, contents)) |
| 40 | } |
| 41 | |
| 42 | pub(crate) fn approved_project_hooks( |
| 43 | workspace: &Path, |
| 44 | ) -> Result<(ProjectHookAuthority, String), String> { |
| 45 | if !crate::config::is_workspace_trusted(workspace) { |
| 46 | return Err("Project hooks require workspace trust and separate hook approval".into()); |
| 47 | } |
| 48 | let (authority, contents) = review_project_hooks(workspace)?; |
| 49 | if crate::config::hook_receipt_for_workspace(&authority.workspace).as_deref() |
| 50 | != Some(&authority.digest) |
| 51 | { |
| 52 | return Err("Project hooks are unapproved or changed; use /hooks review, then /hooks approve <digest>".into()); |
| 53 | } |
| 54 | Ok((authority, contents)) |
| 55 | } |
| 56 | |
| 57 | pub(crate) fn verify_hook_authorities( |
| 58 | plugin: Option<&crate::plugins::types::PluginAuthority>, |
| 59 | project: Option<&ProjectHookAuthority>, |
| 60 | ) -> Result<(), String> { |
| 61 | if let Some(authority) = plugin { |
| 62 | crate::plugins::registry::verify_plugin_component_authority( |
| 63 | authority, |
| 64 | crate::plugins::activation::PluginActivationCapability::Hooks, |
| 65 | )?; |
| 66 | } |
| 67 | if let Some(authority) = project { |
| 68 | let (current, _) = approved_project_hooks(&authority.workspace)?; |
| 69 | if ¤t != authority { |
| 70 | return Err("Project hooks changed after loading; review and approve again".into()); |
| 71 | } |
| 72 | } |
| 73 | Ok(()) |
| 74 | } |
| 75 | |
| 76 | pub(crate) fn approve_project_hooks(workspace: &Path, reviewed_digest: &str) -> Result<(), String> { |
| 77 | if !crate::config::is_workspace_trusted(workspace) { |
| 78 | return Err("Trust the workspace before approving its hooks".into()); |
| 79 | } |
| 80 | let (authority, contents) = review_project_hooks(workspace)?; |
| 81 | if reviewed_digest != authority.digest { |
| 82 | return Err("Hooks do not match the reviewed digest; use /hooks review again".into()); |
| 83 | } |
| 84 | let parsed = |
| 85 | toml::from_str::<super::HooksConfig>(&contents).map_err(|_| "Invalid hooks TOML")?; |
| 86 | if parsed.validate().iter().any(|problem| problem.rejected) { |
| 87 | return Err("Hooks contain invalid entries; correct them before approval".into()); |
| 88 | } |
| 89 | crate::config::save_workspace_hook_receipt(&authority.workspace, reviewed_digest) |
| 90 | .map_err(|_| "Could not save hook approval in user config".to_string())?; |
| 91 | Ok(()) |
| 92 | } |
| 93 |