lfsx-server 1.23.3

A fast, lightweight, secure Git LFS server
Documentation
use jsonwebtoken::{Algorithm, EncodingKey, Header};

use crate::config::GcsCredential;
use crate::error::Error;
use crate::storage::s3::keyspace::token::{Cached, Issued, issued};

const SCOPE: &str = "https://www.googleapis.com/auth/devstorage.read_write";
const METADATA_HOST: &str = "metadata.google.internal";
const METADATA_TOKEN: &str = "/computeMetadata/v1/instance/service-accounts/default/token";
const ASSERTION_LIFETIME: i64 = 3600;

pub(crate) struct ServiceAccount {
    pub(crate) email: String,
    pub(crate) key: EncodingKey,
    token_uri: String,
}

pub(crate) enum Source {
    ServiceAccount(ServiceAccount),
    Metadata { endpoint: String },
    Anonymous,
}

pub(crate) struct Credential {
    source: Source,
    cached: Cached,
}

#[derive(serde::Deserialize)]
struct KeyFile {
    client_email: String,
    private_key: String,
    token_uri: String,
}

#[derive(serde::Serialize)]
struct Claims<'a> {
    iss: &'a str,
    scope: &'a str,
    aud: &'a str,
    iat: i64,
    exp: i64,
}

impl ServiceAccount {
    pub(crate) fn from_json(json: &str) -> Result<Self, Error> {
        let file: KeyFile = serde_json::from_str(json).map_err(|_| {
            Error::Misconfigured("LFSX_GCS_CREDENTIALS is not a service account key file")
        })?;

        Ok(Self {
            email: file.client_email,
            key: EncodingKey::from_rsa_pem(file.private_key.as_bytes()).map_err(|_| {
                Error::Misconfigured("the service account key file holds no usable RSA key")
            })?,
            token_uri: file.token_uri,
        })
    }

    fn assertion(&self) -> Result<String, Error> {
        let now = time::OffsetDateTime::now_utc().unix_timestamp();
        let claims = Claims {
            iss: &self.email,
            scope: SCOPE,
            aud: &self.token_uri,
            iat: now,
            exp: now + ASSERTION_LIFETIME,
        };

        jsonwebtoken::encode(&Header::new(Algorithm::RS256), &claims, &self.key).map_err(|_| {
            Error::Storage(std::io::Error::other(
                "the service account key could not sign a token request",
            ))
        })
    }
}

impl Credential {
    pub(crate) fn new(credential: &GcsCredential) -> Result<Self, Error> {
        let source = match credential {
            GcsCredential::ServiceAccount(path) => {
                let json = std::fs::read_to_string(path).map_err(|_| {
                    Error::Misconfigured("LFSX_GCS_CREDENTIALS names a file that cannot be read")
                })?;
                Source::ServiceAccount(ServiceAccount::from_json(&json)?)
            }
            GcsCredential::Metadata => {
                let host = std::env::var("GCE_METADATA_HOST")
                    .ok()
                    .filter(|host| !host.is_empty())
                    .unwrap_or_else(|| METADATA_HOST.into());
                Source::Metadata {
                    endpoint: format!("http://{host}{METADATA_TOKEN}"),
                }
            }
            GcsCredential::Anonymous => Source::Anonymous,
        };

        Ok(Self::from_source(source))
    }

    pub(crate) fn from_source(source: Source) -> Self {
        Self {
            source,
            cached: Cached::default(),
        }
    }

    pub(crate) fn service_account(&self) -> Option<&ServiceAccount> {
        match &self.source {
            Source::ServiceAccount(account) => Some(account),
            _ => None,
        }
    }

    pub(crate) async fn bearer(&self, client: &reqwest::Client) -> Result<Option<String>, Error> {
        if matches!(self.source, Source::Anonymous) {
            return Ok(None);
        }

        self.cached.get(self.fetch(client)).await.map(Some)
    }

    async fn fetch(&self, client: &reqwest::Client) -> Result<Issued, Error> {
        let request = match &self.source {
            Source::Anonymous => {
                return Err(Error::Storage(std::io::Error::other(
                    "an anonymous credential has no token to fetch",
                )));
            }
            Source::Metadata { endpoint } => {
                client.get(endpoint).header("Metadata-Flavor", "Google")
            }
            Source::ServiceAccount(account) => {
                let body = form_urlencoded::Serializer::new(String::new())
                    .append_pair("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer")
                    .append_pair("assertion", &account.assertion()?)
                    .finish();

                client
                    .post(&account.token_uri)
                    .header(
                        reqwest::header::CONTENT_TYPE,
                        "application/x-www-form-urlencoded",
                    )
                    .body(body)
            }
        };

        issued(request).await
    }
}