lfsx-server 0.20.0

A fast, lightweight, secure Git LFS server
Documentation
use axum::http::StatusCode;
use serde::Deserialize;

use super::Permission;
use crate::error::Error;
use crate::namespace::Namespace;

#[derive(Deserialize)]
struct Project {
    permissions: Option<Permissions>,
}

#[derive(Deserialize)]
struct Permissions {
    project_access: Option<Access>,
    group_access: Option<Access>,
}

#[derive(Deserialize)]
struct Access {
    access_level: u32,
}

#[derive(Deserialize)]
struct User {
    username: String,
}

const REPORTER: u32 = 20;
const DEVELOPER: u32 = 30;
const MAINTAINER: u32 = 40;

pub async fn permission(
    client: &reqwest::Client,
    api_url: &str,
    token: &str,
    ns: &Namespace,
) -> Result<Permission, Error> {
    let url = format!(
        "{api_url}/projects/{}%2F{}",
        urlencoding(ns.org()),
        urlencoding(ns.repo())
    );

    let response = send(client, &url, token).await?;
    let project = response.json::<Project>().await.map_err(|error| {
        tracing::warn!(%error, %url, "forge response could not be parsed");
        Error::Forge
    })?;

    let level = project
        .permissions
        .map(|permissions| {
            let of = |access: Option<Access>| access.map(|a| a.access_level).unwrap_or_default();
            of(permissions.project_access).max(of(permissions.group_access))
        })
        .unwrap_or_default();

    match level {
        level if level >= MAINTAINER => Ok(Permission::Admin),
        level if level >= DEVELOPER => Ok(Permission::Write),
        level if level >= REPORTER => Ok(Permission::Read),
        _ => Err(Error::Forbidden),
    }
}

pub async fn login(client: &reqwest::Client, api_url: &str, token: &str) -> Result<String, Error> {
    let url = format!("{api_url}/user");

    send(client, &url, token)
        .await?
        .json::<User>()
        .await
        .map(|user| user.username)
        .map_err(|error| {
            tracing::warn!(%error, %url, "forge response could not be parsed");
            Error::Forge
        })
}

async fn send(
    client: &reqwest::Client,
    url: &str,
    token: &str,
) -> Result<reqwest::Response, Error> {
    let response = client
        .get(url)
        .bearer_auth(token)
        .send()
        .await
        .map_err(|error| {
            tracing::warn!(%error, %url, "forge request failed");
            Error::Forge
        })?;

    match response.status() {
        StatusCode::OK => Ok(response),
        StatusCode::UNAUTHORIZED => Err(Error::Unauthenticated),
        StatusCode::TOO_MANY_REQUESTS => {
            tracing::warn!(%url, "forge rate limit hit while resolving permissions");
            Err(Error::Forge)
        }
        StatusCode::FORBIDDEN | StatusCode::NOT_FOUND => Err(Error::Forbidden),
        status => {
            tracing::warn!(%status, %url, "unexpected forge response");
            Err(Error::Forge)
        }
    }
}

fn urlencoding(segment: &str) -> String {
    segment
        .bytes()
        .map(|byte| match byte {
            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' => {
                (byte as char).to_string()
            }
            other => format!("%{other:02X}"),
        })
        .collect()
}

#[cfg(test)]
mod tests;