use axum::http::StatusCode;
use serde::Deserialize;
pub mod app;
use super::Permission;
use crate::error::Error;
use crate::namespace::Namespace;
#[derive(Deserialize)]
struct Repository {
permissions: Option<Permissions>,
}
#[derive(Deserialize)]
struct Permissions {
#[serde(default)]
push: bool,
#[serde(default)]
admin: bool,
}
#[derive(Deserialize)]
struct User {
login: String,
}
#[derive(Deserialize)]
struct Visibility {
private: bool,
}
pub async fn permission(
client: &reqwest::Client,
api_url: &str,
token: &str,
ns: &Namespace,
) -> Result<Permission, Error> {
let url = format!("{api_url}/repos/{ns}");
let repository = send(
client,
&url,
token,
"the forge will not admit this repository to this token",
)
.await?
.json::<Repository>()
.await
.map_err(|error| {
tracing::warn!(%error, %url, "forge response could not be parsed");
Error::Forge
})?;
match repository.permissions {
Some(Permissions { admin: true, .. }) => Ok(Permission::Admin),
Some(Permissions { push: true, .. }) => Ok(Permission::Write),
_ => Ok(Permission::Read),
}
}
pub async fn public(
client: &reqwest::Client,
api_url: &str,
app: Option<&app::App>,
ns: &Namespace,
) -> Result<Permission, Error> {
let url = format!("{api_url}/repos/{ns}");
let grant = match app {
Some(app) => app.token(client, api_url, ns).await?,
None => None,
};
let identified = grant.is_some();
let asked = match grant {
Some(token) => asking(client, &url).bearer_auth(token),
None => asking(client, &url),
};
let response = crate::telemetry::propagated(asked)
.send()
.await
.map_err(|error| {
tracing::warn!(%error, %url, "forge request failed");
Error::Forge
})?;
match response.status() {
StatusCode::OK if identified => {
let visible: Visibility = response.json().await.map_err(|error| {
tracing::warn!(%error, %url, "forge response could not be parsed");
Error::Forge
})?;
if visible.private {
tracing::info!(%url, "the repository is private, so anonymous read stays refused");
return Err(Error::Unauthenticated);
}
Ok(Permission::Read)
}
StatusCode::OK => Ok(Permission::Read),
StatusCode::NOT_FOUND => {
tracing::info!(%url, "the forge will not admit this repository anonymously");
Err(Error::Unauthenticated)
}
StatusCode::UNAUTHORIZED => {
tracing::warn!(%url, "the forge refused this server's anonymous lookup");
Err(Error::Unauthenticated)
}
StatusCode::FORBIDDEN | StatusCode::TOO_MANY_REQUESTS
if let Some(retry_after) = super::backoff::rate_limited(&response) =>
{
tracing::warn!(%url, retry_after, "forge is rate-limiting this server");
Err(Error::RateLimited { retry_after })
}
status => {
tracing::warn!(%status, %url, "unexpected forge response to an anonymous lookup");
Err(Error::Unauthenticated)
}
}
}
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,
"the forge will not say who this token belongs to",
)
.await?
.json::<User>()
.await
.map(|user| user.login)
.map_err(|error| {
tracing::warn!(%error, %url, "forge response could not be parsed");
Error::Forge
})
}
fn asking(client: &reqwest::Client, url: &str) -> reqwest::RequestBuilder {
client
.get(url)
.header("accept", "application/vnd.github+json")
.header("x-github-api-version", "2022-11-28")
}
async fn send(
client: &reqwest::Client,
url: &str,
token: &str,
refusal: &'static str,
) -> Result<reqwest::Response, Error> {
let response = crate::telemetry::propagated(asking(client, 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::FORBIDDEN | StatusCode::TOO_MANY_REQUESTS
if let Some(retry_after) = super::backoff::rate_limited(&response) =>
{
tracing::warn!(%url, retry_after, "forge is rate-limiting this server");
Err(Error::RateLimited { retry_after })
}
StatusCode::FORBIDDEN | StatusCode::NOT_FOUND => {
tracing::info!(%url, "{refusal}");
Err(Error::Forbidden)
}
status => {
tracing::warn!(%status, %url, "unexpected forge response");
Err(Error::Forge)
}
}
}
#[cfg(test)]
mod tests;