| 1 | |
| 2 | |
| 3 | |
| 4 | use std::path::PathBuf; |
| 5 | |
| 6 | use forge_core::{Action, Denied, Repo, User, authorize}; |
| 7 | use forge_git::{GitCommand, GitService}; |
| 8 | |
| 9 | use crate::state::AppState; |
| 10 | |
| 11 | #[derive(Debug, PartialEq, Eq)] |
| 12 | pub enum AccessError { |
| 13 | |
| 14 | Unauthorized, |
| 15 | |
| 16 | NotFound, |
| 17 | |
| 18 | Forbidden, |
| 19 | Internal(String), |
| 20 | } |
| 21 | |
| 22 | pub enum Transport { |
| 23 | Http, |
| 24 | Ssh, |
| 25 | } |
| 26 | |
| 27 | impl Transport { |
| 28 | pub fn as_str(&self) -> &'static str { |
| 29 | match self { |
| 30 | Transport::Http => "http", |
| 31 | Transport::Ssh => "ssh", |
| 32 | } |
| 33 | } |
| 34 | } |
| 35 | |
| 36 | fn internal(e: impl std::fmt::Display) -> AccessError { |
| 37 | AccessError::Internal(e.to_string()) |
| 38 | } |
| 39 | |
| 40 | |
| 41 | |
| 42 | fn hidden_or_missing(actor: Option<&User>) -> AccessError { |
| 43 | if actor.is_none() { |
| 44 | AccessError::Unauthorized |
| 45 | } else { |
| 46 | AccessError::NotFound |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | |
| 51 | |
| 52 | pub async fn resolve_repo( |
| 53 | state: &AppState, |
| 54 | actor: Option<&User>, |
| 55 | owner: &str, |
| 56 | name: &str, |
| 57 | service: GitService, |
| 58 | ) -> Result<(Repo, PathBuf), AccessError> { |
| 59 | let action = match service { |
| 60 | GitService::UploadPack => Action::Read, |
| 61 | GitService::ReceivePack => Action::Write, |
| 62 | }; |
| 63 | |
| 64 | |
| 65 | if let Some(base) = forge_core::names::wiki_base(name) { |
| 66 | let repo = resolve_repo_for(state, actor, owner, base, action).await?; |
| 67 | if !state.store.wiki_enabled(&repo.id).await.map_err(internal)? { |
| 68 | return Err(hidden_or_missing(actor)); |
| 69 | } |
| 70 | let path = |
| 71 | forge_git::repo::wiki_path(&state.config.data_dir, &repo.id).map_err(internal)?; |
| 72 | return Ok((repo, path)); |
| 73 | } |
| 74 | let repo = resolve_repo_for(state, actor, owner, name, action).await?; |
| 75 | let path = forge_git::repo::repo_path(&state.config.data_dir, &repo.id).map_err(internal)?; |
| 76 | Ok((repo, path)) |
| 77 | } |
| 78 | |
| 79 | |
| 80 | |
| 81 | pub async fn find_repo( |
| 82 | state: &AppState, |
| 83 | owner: &str, |
| 84 | name: &str, |
| 85 | ) -> Result<Option<Repo>, AccessError> { |
| 86 | let name = name.strip_suffix(".git").unwrap_or(name); |
| 87 | |
| 88 | |
| 89 | |
| 90 | match state |
| 91 | .store |
| 92 | .repo_by_owner_and_name(owner, name) |
| 93 | .await |
| 94 | .map_err(internal)? |
| 95 | { |
| 96 | Some(repo) => Ok(Some(repo)), |
| 97 | None => state |
| 98 | .store |
| 99 | .repo_by_redirect(owner, name) |
| 100 | .await |
| 101 | .map_err(internal), |
| 102 | } |
| 103 | } |
| 104 | |
| 105 | |
| 106 | |
| 107 | pub async fn resolve_repo_for( |
| 108 | state: &AppState, |
| 109 | actor: Option<&User>, |
| 110 | owner: &str, |
| 111 | name: &str, |
| 112 | action: Action, |
| 113 | ) -> Result<Repo, AccessError> { |
| 114 | let Some(repo) = find_repo(state, owner, name).await? else { |
| 115 | return Err(hidden_or_missing(actor)); |
| 116 | }; |
| 117 | |
| 118 | let (role, org_role) = match actor { |
| 119 | Some(user) => ( |
| 120 | state |
| 121 | .store |
| 122 | .collaborator_role(&repo.id, &user.id) |
| 123 | .await |
| 124 | .map_err(internal)?, |
| 125 | state |
| 126 | .store |
| 127 | .org_repo_role(&repo.owner_id, &user.id, &repo.id) |
| 128 | .await |
| 129 | .map_err(internal)?, |
| 130 | ), |
| 131 | None => (None, None), |
| 132 | }; |
| 133 | |
| 134 | authorize(actor, &repo, role, org_role, action).map_err(|denied| match denied { |
| 135 | Denied::NotFound => hidden_or_missing(actor), |
| 136 | Denied::Forbidden if actor.is_none() => AccessError::Unauthorized, |
| 137 | Denied::Forbidden => AccessError::Forbidden, |
| 138 | })?; |
| 139 | |
| 140 | Ok(repo) |
| 141 | } |
| 142 | |
| 143 | |
| 144 | |
| 145 | |
| 146 | |
| 147 | #[allow(clippy::too_many_arguments)] |
| 148 | pub fn git_process( |
| 149 | state: &AppState, |
| 150 | repo: &Repo, |
| 151 | repo_path: &std::path::Path, |
| 152 | actor: Option<&User>, |
| 153 | service: GitService, |
| 154 | transport: Transport, |
| 155 | advertise_refs: bool, |
| 156 | git_protocol: Option<&str>, |
| 157 | ) -> GitCommand { |
| 158 | let stateless_rpc = matches!(transport, Transport::Http); |
| 159 | let cmd = forge_git::service_command( |
| 160 | &state.git, |
| 161 | service, |
| 162 | repo_path, |
| 163 | stateless_rpc, |
| 164 | advertise_refs, |
| 165 | git_protocol, |
| 166 | ); |
| 167 | |
| 168 | if service != GitService::ReceivePack { |
| 169 | return cmd; |
| 170 | } |
| 171 | let is_wiki = repo_path |
| 172 | .file_name() |
| 173 | .and_then(|n| n.to_str()) |
| 174 | .is_some_and(|n| n.ends_with(".wiki.git")); |
| 175 | |
| 176 | let actor_id = actor.map_or("", |user| user.id.as_str()); |
| 177 | cmd.env("FORGE_INTERNAL_SOCKET", state.internal_socket.as_os_str()) |
| 178 | .env("FORGE_INTERNAL_TOKEN", &*state.internal_token) |
| 179 | .env("FORGE_ACTOR_ID", actor_id) |
| 180 | .env("FORGE_REPO_ID", repo.id.as_str()) |
| 181 | .env("FORGE_PROTOCOL", transport.as_str()) |
| 182 | .env("FORGE_WIKI", if is_wiki { "1" } else { "0" }) |
| 183 | } |