Skip to main content

git_lfs_api/
ssh.rs

1//! SSH-based endpoint resolution hook.
2//!
3//! When the LFS endpoint is reached via SSH (e.g. `lfs.url =
4//! ssh://...`, or a `git@host:repo` remote without a separate
5//! `lfs.url`), upstream Git LFS shells out to `git-lfs-authenticate` to
6//! obtain a replacement HTTPS URL plus auth headers. This crate is
7//! transport-agnostic, so it expresses the hook as a [`SshResolver`]
8//! trait — the actual `ssh` invocation lives in `git-lfs-creds`.
9//!
10//! The [`Client`](crate::Client) consults the resolver once per
11//! request: a non-empty [`SshAuth::href`] overrides the endpoint URL
12//! prefix for that call, and [`SshAuth::headers`] are merged into the
13//! outgoing request. Caching is the resolver's responsibility — see
14//! `git_lfs_creds::SshAuthClient` for the production implementation.
15
16use std::collections::HashMap;
17use std::sync::Arc;
18
19use crate::error::ApiError;
20
21/// `git-lfs-authenticate <path> <op>` operation argument. Wire form is
22/// lowercase `upload` or `download`.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub enum SshOperation {
25    /// Auth scope needed to push objects to the server.
26    Upload,
27    /// Auth scope needed to fetch objects from the server.
28    Download,
29}
30
31impl SshOperation {
32    /// Default mirrors upstream's `endpointOperation`: GET/HEAD →
33    /// download, anything else → upload. Used as the fallback when a
34    /// caller doesn't pass an explicit operation.
35    pub fn from_http_method(method: &reqwest::Method) -> Self {
36        if matches!(*method, reqwest::Method::GET | reqwest::Method::HEAD) {
37            Self::Download
38        } else {
39            Self::Upload
40        }
41    }
42}
43
44/// Resolved auth from a `git-lfs-authenticate` call.
45#[derive(Debug, Clone, Default)]
46pub struct SshAuth {
47    /// Replacement endpoint URL prefix.
48    ///
49    /// Empty (`""`) when the server expects the original URL to be used as-is.
50    pub href: String,
51    /// Headers to merge into the outgoing request.
52    ///
53    /// Typically a single `Authorization` entry, but the schema lets servers
54    /// set arbitrary keys (e.g. `X-RemoteAuth-Provider` for vendor extensions).
55    pub headers: HashMap<String, String>,
56}
57
58/// Hook called once per LFS API request to resolve SSH-mediated auth.
59///
60/// Implementations are typically backed by a `git-lfs-authenticate`
61/// invocation with a small in-memory cache keyed on `(host, path,
62/// operation)` so the SSH command runs at most once per cache TTL.
63pub trait SshResolver: Send + Sync {
64    /// Return the auth response for `operation`. `Ok(default)` (empty
65    /// `href`, empty `headers`) means "no SSH override — use the
66    /// configured endpoint with whatever auth is already on the
67    /// request".
68    fn resolve(&self, operation: SshOperation) -> Result<SshAuth, ApiError>;
69}
70
71/// Type alias for the boxed-resolver field on [`Client`](crate::Client).
72pub type SharedSshResolver = Arc<dyn SshResolver>;