Private subrepos clone with the parent's credentials for the same hub a07f60a37fb2
A subrepo on the same hub (scheme, host and port) as one of the parent's remotes is probed and cloned with that remote's token and CA, which are recorded on the subrepo's origin so it fetches, pushes and works with oh afterwards; otherwise the environment's OXY_TOKEN and OXY_CA are used as before and not recorded. So oxy submodule update --init of a private subrepo works for anyone who can read the parent (metalgear feedback 23). closes #32
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-09-30 14:06 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
a07f60a37fb21005b17ecb3bcca6fb906c70307110b9211030ebaa9246bc8ada- Depends on
- 0955469ac0e1 2b5ee371060e 4dee277c327a 982864068a59 b3c5a59a875a d77e1702d770
- Depended on by
- d36ba2f79f95 9f9e72b93e4e ff1f62fee654 ce14f0d68b15
- In bookmarks
- agents-in-readme bare-hashes carried-not-removed carry-identical doctor-trust identical-sides keyless-tags main merge-remote-bookmark only-help-103 paths-for-resolutions-and-amend push-notes show-stat subrepo-pin-state switch-says
- In tags
- v0.1.0 v0.10.0 v0.11.0 v0.2.0 v0.3.0 v0.3.1 v0.4.0 v0.4.1 v0.4.2 v0.4.3 v0.4.4 v0.4.5 v0.4.6 v0.5.0 v0.5.1 v0.6.0 v0.7.0 v0.8.0 v0.9.0
- Edits
- 10 atoms, 0 tree operations, 3 files
Browse the tree at this change
modified crates/oxy-core/src/repo/subrepos.rs +9 −14
| … | 107 unchanged lines |
||
| 108 | 108 | Ok(!git_out(dir, &["status", "--porcelain"])?.is_empty()) |
|
| 109 | 109 | } |
|
| 110 | 110 | ||
| 111 | - | /// Whether `url` names an Oxygen repository (else it is taken to be Git). |
|
| 112 | - | /// Whether `url` is an Oxygen repository, probed with the environment's |
|
| 113 | - | /// credentials (`OXY_TOKEN`, `OXY_CA`) as a clone would use them; with the |
|
| 114 | - | /// probe's error when it is not, to report if Git cannot clone it either. |
|
| 115 | - | fn probe_oxy_url(url: &str) -> std::result::Result<(), String> { |
|
| 111 | + | /// Whether `url` is an Oxygen repository (else it is taken to be Git), |
|
| 112 | + | /// probed with `options`, the credentials a clone of it would use; with |
|
| 113 | + | /// the probe's error when it is not, to report if Git cannot clone it |
|
| 114 | + | /// either. |
|
| 115 | + | fn probe_oxy_url(url: &str, options: &RemoteOptions) -> std::result::Result<(), String> { |
|
| 116 | 116 | if url.starts_with("http://") || url.starts_with("https://") { |
|
| 117 | - | let options = RemoteOptions { |
|
| 118 | - | token: std::env::var("OXY_TOKEN").ok(), |
|
| 119 | - | ca: std::env::var("OXY_CA").ok().map(std::path::PathBuf::from), |
|
| 120 | - | repo_id: None, |
|
| 121 | - | }; |
|
| 122 | - | return crate::protocol::open_remote(url, &options) |
|
| 117 | + | return crate::protocol::open_remote(url, options) |
|
| 123 | 118 | .and_then(|r| r.repo_id()) |
|
| 124 | 119 | .map(|_| ()) |
|
| 125 | 120 | .map_err(|e| e.to_string()); |
|
| … | 42 unchanged lines |
||
| 168 | 163 | } |
|
| 169 | 164 | let _ = fs::remove_dir(&dir); |
|
| 170 | 165 | let r = match pin { |
|
| 171 | - | Pin::Oxy { .. } => Repo::clone(&m.url, &dir).map(|_| ()), |
|
| 166 | + | Pin::Oxy { .. } => self.clone_as_subrepo(&m.url, &dir).map(|_| ()), |
|
| 172 | 167 | Pin::Git { .. } => Command::new("git") |
|
| 173 | 168 | .args(["clone", "-q", &m.url]) |
|
| 174 | 169 | .arg(&dir) |
|
| … | 152 unchanged lines |
||
| 327 | 322 | if let Some(parent) = dir.parent() { |
|
| 328 | 323 | fs::create_dir_all(parent)?; |
|
| 329 | 324 | } |
|
| 330 | - | let probe = probe_oxy_url(url); |
|
| 325 | + | let probe = probe_oxy_url(url, &self.credentials_for(url)); |
|
| 331 | 326 | let git = probe.is_err(); |
|
| 332 | 327 | if git { |
|
| 333 | 328 | let mut cmd = Command::new("git"); |
|
| … | 14 unchanged lines |
||
| 348 | 343 | } |
|
| 349 | 344 | } else { |
|
| 350 | 345 | let _ = fs::remove_dir(&dir); |
|
| 351 | - | let mut nested = Repo::clone(url, &dir)?; |
|
| 346 | + | let mut nested = self.clone_as_subrepo(url, &dir)?; |
|
| 352 | 347 | if let Some(b) = bookmark { |
|
| 353 | 348 | nested.bookmark_switch(b, true)?; |
|
| 354 | 349 | } |
|
| … | 74 unchanged lines |
modified crates/oxy-core/src/repo/sync.rs +69 −1
| … | 1684 unchanged lines |
||
| 1685 | 1685 | ca: std::env::var("OXY_CA").ok().map(PathBuf::from), |
|
| 1686 | 1686 | repo_id: None, |
|
| 1687 | 1687 | }; |
|
| 1688 | - | let remote = open_remote(url, &options)?; |
|
| 1688 | + | Self::clone_using(url, dest, max_blob, sparse, depth, &options, false) |
|
| 1689 | + | } |
|
| 1690 | + | ||
| 1691 | + | /// Clone a subrepo with credentials the parent has for the same hub |
|
| 1692 | + | /// (see [`Repo::credentials_for`]), recording them on its `origin` so it |
|
| 1693 | + | /// fetches and pushes with them after. |
|
| 1694 | + | pub(crate) fn clone_as_subrepo(&self, url: &str, dest: &Path) -> Result<Repo> { |
|
| 1695 | + | // Only credentials from a remote are recorded; the environment's |
|
| 1696 | + | // stay in the environment. |
|
| 1697 | + | let borrowed = self.remote_on_same_hub(url).is_some(); |
|
| 1698 | + | let options = self.credentials_for(url); |
|
| 1699 | + | Self::clone_using(url, dest, None, &[], None, &options, borrowed) |
|
| 1700 | + | } |
|
| 1701 | + | ||
| 1702 | + | /// The name of a remote of this repository on the same hub (scheme, |
|
| 1703 | + | /// host and port) as `url`. |
|
| 1704 | + | fn remote_on_same_hub(&self, url: &str) -> Option<String> { |
|
| 1705 | + | let origin = |u: &str| -> Option<String> { |
|
| 1706 | + | let (scheme, rest) = u.split_once("://")?; |
|
| 1707 | + | let host = rest.split('/').next()?; |
|
| 1708 | + | Some(format!( |
|
| 1709 | + | "{}://{}", |
|
| 1710 | + | scheme.to_ascii_lowercase(), |
|
| 1711 | + | host.to_ascii_lowercase() |
|
| 1712 | + | )) |
|
| 1713 | + | }; |
|
| 1714 | + | let want = origin(url)?; |
|
| 1715 | + | self.config |
|
| 1716 | + | .remotes |
|
| 1717 | + | .iter() |
|
| 1718 | + | .find(|(_, u)| origin(u).as_deref() == Some(want.as_str())) |
|
| 1719 | + | .map(|(name, _)| name.clone()) |
|
| 1720 | + | } |
|
| 1721 | + | ||
| 1722 | + | /// The token and CA to reach `url` with: those of a remote of this |
|
| 1723 | + | /// repository on the same hub (scheme, host and port), else the |
|
| 1724 | + | /// environment's (`OXY_TOKEN`, `OXY_CA`), as a clone would use them. |
|
| 1725 | + | pub(crate) fn credentials_for(&self, url: &str) -> RemoteOptions { |
|
| 1726 | + | match self.remote_on_same_hub(url) { |
|
| 1727 | + | Some(name) => RemoteOptions { |
|
| 1728 | + | repo_id: None, |
|
| 1729 | + | ..self.remote_options(&name) |
|
| 1730 | + | }, |
|
| 1731 | + | None => RemoteOptions { |
|
| 1732 | + | token: std::env::var("OXY_TOKEN").ok(), |
|
| 1733 | + | ca: std::env::var("OXY_CA").ok().map(PathBuf::from), |
|
| 1734 | + | repo_id: None, |
|
| 1735 | + | }, |
|
| 1736 | + | } |
|
| 1737 | + | } |
|
| 1738 | + | ||
| 1739 | + | fn clone_using( |
|
| 1740 | + | url: &str, |
|
| 1741 | + | dest: &Path, |
|
| 1742 | + | max_blob: Option<u64>, |
|
| 1743 | + | sparse: &[String], |
|
| 1744 | + | depth: Option<u32>, |
|
| 1745 | + | options: &RemoteOptions, |
|
| 1746 | + | record: bool, |
|
| 1747 | + | ) -> Result<Repo> { |
|
| 1748 | + | let remote = open_remote(url, options)?; |
|
| 1689 | 1749 | let id = remote.repo_id()?; |
|
| 1690 | 1750 | if id == crate::protocol::UNBOUND_ID { |
|
| 1691 | 1751 | return fail(format!( |
|
| … | 2 unchanged lines |
||
| 1694 | 1754 | } |
|
| 1695 | 1755 | let mut repo = Repo::init_with_id(dest, id)?; |
|
| 1696 | 1756 | repo.config.set("remote.origin.url", url)?; |
|
| 1757 | + | if record { |
|
| 1758 | + | if let Some(t) = &options.token { |
|
| 1759 | + | repo.config.set("remote.origin.token", t)?; |
|
| 1760 | + | } |
|
| 1761 | + | if let Some(ca) = &options.ca { |
|
| 1762 | + | repo.config.set("remote.origin.ca", &ca.to_string_lossy())?; |
|
| 1763 | + | } |
|
| 1764 | + | } |
|
| 1697 | 1765 | if let Some(m) = max_blob { |
|
| 1698 | 1766 | repo.config.set("filter.max-blob", &m.to_string())?; |
|
| 1699 | 1767 | } else if !sparse.is_empty() { |
|
| … | 40 unchanged lines |
modified crates/oxy-core/src/repo/tests.rs +31 −0
| … | 6198 unchanged lines |
||
| 6199 | 6199 | rep.problems |
|
| 6200 | 6200 | ); |
|
| 6201 | 6201 | } |
|
| 6202 | + | ||
| 6203 | + | /// oxygen#32: a subrepo on the same hub as one of the parent's remotes is |
|
| 6204 | + | /// reached with that remote's token and CA (a private subrepo then clones |
|
| 6205 | + | /// for anyone who can read the parent); another hub gets none of them. |
|
| 6206 | + | #[test] |
|
| 6207 | + | fn a_subrepo_on_the_parents_hub_borrows_its_credentials() { |
|
| 6208 | + | hermetic(); |
|
| 6209 | + | let dir = tempfile::tempdir().unwrap(); |
|
| 6210 | + | let mut repo = Repo::init(dir.path()).unwrap(); |
|
| 6211 | + | repo.config |
|
| 6212 | + | .set( |
|
| 6213 | + | "remote.origin.url", |
|
| 6214 | + | "https://Hub.example:7343/doomsdayone/metalgear", |
|
| 6215 | + | ) |
|
| 6216 | + | .unwrap(); |
|
| 6217 | + | repo.config.set("remote.origin.token", "secret").unwrap(); |
|
| 6218 | + | repo.config |
|
| 6219 | + | .set("remote.origin.ca", "/tmp/hub-ca.pem") |
|
| 6220 | + | .unwrap(); |
|
| 6221 | + | let same = repo.credentials_for("https://hub.example:7343/doomsdayone/rstar-api"); |
|
| 6222 | + | assert_eq!(same.token.as_deref(), Some("secret")); |
|
| 6223 | + | assert_eq!(same.ca.as_deref(), Some(Path::new("/tmp/hub-ca.pem"))); |
|
| 6224 | + | for other in [ |
|
| 6225 | + | "https://elsewhere.example:7343/doomsdayone/rstar-api", |
|
| 6226 | + | "https://hub.example/doomsdayone/rstar-api", |
|
| 6227 | + | "http://hub.example:7343/doomsdayone/rstar-api", |
|
| 6228 | + | ] { |
|
| 6229 | + | let o = repo.credentials_for(other); |
|
| 6230 | + | assert_ne!(o.token.as_deref(), Some("secret"), "{other}"); |
|
| 6231 | + | } |
|
| 6232 | + | } |