1
0
Fork 0

add more auth options

This commit is contained in:
Cyborus 2023-12-20 13:42:33 -05:00
parent bfb341c3e6
commit 9908d5086c
No known key found for this signature in database
3 changed files with 90 additions and 12 deletions

14
Cargo.lock generated
View file

@ -44,6 +44,12 @@ version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
[[package]]
name = "base64ct"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@ -169,6 +175,7 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
name = "forgejo-api" name = "forgejo-api"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"base64ct",
"bytes", "bytes",
"eyre", "eyre",
"reqwest", "reqwest",
@ -179,6 +186,7 @@ dependencies = [
"time", "time",
"tokio", "tokio",
"url", "url",
"zeroize",
] ]
[[package]] [[package]]
@ -1185,3 +1193,9 @@ dependencies = [
"cfg-if", "cfg-if",
"windows-sys", "windows-sys",
] ]
[[package]]
name = "zeroize"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"

View file

@ -15,6 +15,8 @@ serde = { version = "1.0.168", features = ["derive"] }
time = { version = "0.3.22", features = ["parsing", "serde", "formatting"] } time = { version = "0.3.22", features = ["parsing", "serde", "formatting"] }
serde_json = "1.0.108" serde_json = "1.0.108"
bytes = "1.5.0" bytes = "1.5.0"
base64ct = "1.6.0"
zeroize = "1.7.0"
[dev-dependencies] [dev-dependencies]
eyre = "0.6.9" eyre = "0.6.9"

View file

@ -2,6 +2,7 @@ use reqwest::{Client, Request, StatusCode};
use serde::{de::DeserializeOwned, Serialize}; use serde::{de::DeserializeOwned, Serialize};
use soft_assert::*; use soft_assert::*;
use url::Url; use url::Url;
use zeroize::Zeroize;
pub struct Forgejo { pub struct Forgejo {
url: Url, url: Url,
@ -42,29 +43,90 @@ pub enum ForgejoError {
UnexpectedStatusCode(StatusCode), UnexpectedStatusCode(StatusCode),
#[error("{} {}: {}", .0.as_u16(), .0.canonical_reason().unwrap_or(""), .1)] #[error("{} {}: {}", .0.as_u16(), .0.canonical_reason().unwrap_or(""), .1)]
ApiError(StatusCode, String), ApiError(StatusCode, String),
#[error("the provided authorization was too long to accept")]
AuthTooLong,
}
/// Method of authentication to connect to the Forgejo host with.
pub enum Auth<'a> {
/// Application Access Token. Grants access to scope enabled for the
/// provided token, which may include full access.
///
/// To learn how to create a token, see
/// [the Codeberg docs on the subject](https://docs.codeberg.org/advanced/access-token/).
///
/// To learn about token scope, see
/// [the official Forgejo docs](https://forgejo.org/docs/latest/user/token-scope/).
Token(&'a str),
/// Username, password, and 2-factor auth code (if enabled). Grants full
/// access to the user's account.
Password {
username: &'a str,
password: &'a str,
mfa: Option<&'a str>,
},
/// No authentication. Only grants access to access public endpoints.
None,
} }
impl Forgejo { impl Forgejo {
pub fn new(api_key: &str, url: Url) -> Result<Self, ForgejoError> { pub fn new(auth: Auth, url: Url) -> Result<Self, ForgejoError> {
Self::with_user_agent(api_key, url, "forgejo-api-rs") Self::with_user_agent(auth, url, "forgejo-api-rs")
} }
pub fn with_user_agent( pub fn with_user_agent(auth: Auth, url: Url, user_agent: &str) -> Result<Self, ForgejoError> {
api_key: &str,
url: Url,
user_agent: &str,
) -> Result<Self, ForgejoError> {
soft_assert!( soft_assert!(
matches!(url.scheme(), "http" | "https"), matches!(url.scheme(), "http" | "https"),
Err(ForgejoError::HttpRequired) Err(ForgejoError::HttpRequired)
); );
let mut headers = reqwest::header::HeaderMap::new(); let mut headers = reqwest::header::HeaderMap::new();
let mut key_header: reqwest::header::HeaderValue = format!("token {api_key}") match auth {
.try_into() Auth::Token(token) => {
.map_err(|_| ForgejoError::KeyNotAscii)?; let mut header: reqwest::header::HeaderValue = format!("token {token}")
key_header.set_sensitive(true); .try_into()
headers.insert("Authorization", key_header); .map_err(|_| ForgejoError::KeyNotAscii)?;
header.set_sensitive(true);
headers.insert("Authorization", header);
}
Auth::Password {
username,
password,
mfa,
} => {
let len = (((username.len() + password.len() + 1)
.checked_mul(4)
.ok_or(ForgejoError::AuthTooLong)?)
/ 3)
+ 1;
let mut bytes = vec![0; len];
// panic safety: len cannot be zero
let mut encoder = base64ct::Encoder::<base64ct::Base64>::new(&mut bytes).unwrap();
// panic safety: len will always be enough
encoder.encode(username.as_bytes()).unwrap();
encoder.encode(b":").unwrap();
encoder.encode(password.as_bytes()).unwrap();
let b64 = encoder.finish().unwrap();
let mut header: reqwest::header::HeaderValue =
format!("Basic {b64}").try_into().unwrap(); // panic safety: base64 is always ascii
header.set_sensitive(true);
headers.insert("Authorization", header);
bytes.zeroize();
if let Some(mfa) = mfa {
let mut key_header: reqwest::header::HeaderValue =
mfa.try_into().map_err(|_| ForgejoError::KeyNotAscii)?;
key_header.set_sensitive(true);
headers.insert("X-FORGEJO-OTP", key_header);
}
}
Auth::None => (),
}
let client = Client::builder() let client = Client::builder()
.user_agent(user_agent) .user_agent(user_agent)
.default_headers(headers) .default_headers(headers)