Merge pull request 'add authentication options' (#32) from auth into main
Reviewed-on: https://codeberg.org/Cyborus/forgejo-api/pulls/32
This commit is contained in:
commit
c220b8429b
14
Cargo.lock
generated
14
Cargo.lock
generated
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
86
src/lib.rs
86
src/lib.rs
|
@ -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)
|
||||||
|
|
|
@ -5,7 +5,7 @@ use forgejo_api::Forgejo;
|
||||||
async fn ci() -> eyre::Result<()> {
|
async fn ci() -> eyre::Result<()> {
|
||||||
let url = url::Url::parse(&std::env::var("FORGEJO_API_CI_INSTANCE_URL")?)?;
|
let url = url::Url::parse(&std::env::var("FORGEJO_API_CI_INSTANCE_URL")?)?;
|
||||||
let token = std::env::var("FORGEJO_API_CI_TOKEN")?;
|
let token = std::env::var("FORGEJO_API_CI_TOKEN")?;
|
||||||
let api = Forgejo::new(&token, url)?;
|
let api = Forgejo::new(forgejo_api::Auth::Token(&token), url)?;
|
||||||
|
|
||||||
let mut results = Vec::new();
|
let mut results = Vec::new();
|
||||||
|
|
||||||
|
@ -54,6 +54,22 @@ async fn user(api: &forgejo_api::Forgejo) -> eyre::Result<()> {
|
||||||
let followers = api.get_followers("TestingAdmin").await?;
|
let followers = api.get_followers("TestingAdmin").await?;
|
||||||
ensure!(followers == Some(Vec::new()), "follower list not empty");
|
ensure!(followers == Some(Vec::new()), "follower list not empty");
|
||||||
|
|
||||||
|
let url = url::Url::parse(&std::env::var("FORGEJO_API_CI_INSTANCE_URL")?)?;
|
||||||
|
let password_api = Forgejo::new(
|
||||||
|
forgejo_api::Auth::Password {
|
||||||
|
username: "TestingAdmin",
|
||||||
|
password: "password",
|
||||||
|
mfa: None,
|
||||||
|
},
|
||||||
|
url,
|
||||||
|
)
|
||||||
|
.wrap_err("failed to log in using username and password")?;
|
||||||
|
|
||||||
|
ensure!(
|
||||||
|
api.myself().await? == password_api.myself().await?,
|
||||||
|
"users not equal comparing token-auth and pass-auth"
|
||||||
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue