summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorCyborus <cyborus@noreply.codeberg.org>2023-12-20 20:58:22 +0100
committerCyborus <cyborus@noreply.codeberg.org>2023-12-20 20:58:22 +0100
commitc220b8429b4180e25efc81e2cdef1e7526ffd272 (patch)
treedbefca01ba5117c11b93458c95b1ddd84a4c646b
parentMerge pull request 'don't use rustls' (#31) from no-rustls into main (diff)
parentadd password auth testing (diff)
downloadforgejo-api-c220b8429b4180e25efc81e2cdef1e7526ffd272.tar.xz
forgejo-api-c220b8429b4180e25efc81e2cdef1e7526ffd272.zip
Merge pull request 'add authentication options' (#32) from auth into main
Reviewed-on: https://codeberg.org/Cyborus/forgejo-api/pulls/32
-rw-r--r--Cargo.lock14
-rw-r--r--Cargo.toml2
-rw-r--r--src/lib.rs86
-rw-r--r--tests/ci_test.rs18
4 files changed, 107 insertions, 13 deletions
diff --git a/Cargo.lock b/Cargo.lock
index ba933cb..7360368 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -45,6 +45,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
[[package]]
+name = "base64ct"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
+
+[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -169,6 +175,7 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
name = "forgejo-api"
version = "0.1.0"
dependencies = [
+ "base64ct",
"bytes",
"eyre",
"reqwest",
@@ -179,6 +186,7 @@ dependencies = [
"time",
"tokio",
"url",
+ "zeroize",
]
[[package]]
@@ -1185,3 +1193,9 @@ dependencies = [
"cfg-if",
"windows-sys",
]
+
+[[package]]
+name = "zeroize"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
diff --git a/Cargo.toml b/Cargo.toml
index 83ad83c..c53d74d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,6 +15,8 @@ serde = { version = "1.0.168", features = ["derive"] }
time = { version = "0.3.22", features = ["parsing", "serde", "formatting"] }
serde_json = "1.0.108"
bytes = "1.5.0"
+base64ct = "1.6.0"
+zeroize = "1.7.0"
[dev-dependencies]
eyre = "0.6.9"
diff --git a/src/lib.rs b/src/lib.rs
index e548128..1776812 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -2,6 +2,7 @@ use reqwest::{Client, Request, StatusCode};
use serde::{de::DeserializeOwned, Serialize};
use soft_assert::*;
use url::Url;
+use zeroize::Zeroize;
pub struct Forgejo {
url: Url,
@@ -42,29 +43,90 @@ pub enum ForgejoError {
UnexpectedStatusCode(StatusCode),
#[error("{} {}: {}", .0.as_u16(), .0.canonical_reason().unwrap_or(""), .1)]
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 {
- pub fn new(api_key: &str, url: Url) -> Result<Self, ForgejoError> {
- Self::with_user_agent(api_key, url, "forgejo-api-rs")
+ pub fn new(auth: Auth, url: Url) -> Result<Self, ForgejoError> {
+ Self::with_user_agent(auth, url, "forgejo-api-rs")
}
- pub fn with_user_agent(
- api_key: &str,
- url: Url,
- user_agent: &str,
- ) -> Result<Self, ForgejoError> {
+ pub fn with_user_agent(auth: Auth, url: Url, user_agent: &str) -> Result<Self, ForgejoError> {
soft_assert!(
matches!(url.scheme(), "http" | "https"),
Err(ForgejoError::HttpRequired)
);
let mut headers = reqwest::header::HeaderMap::new();
- let mut key_header: reqwest::header::HeaderValue = format!("token {api_key}")
- .try_into()
- .map_err(|_| ForgejoError::KeyNotAscii)?;
- key_header.set_sensitive(true);
- headers.insert("Authorization", key_header);
+ match auth {
+ Auth::Token(token) => {
+ let mut header: reqwest::header::HeaderValue = format!("token {token}")
+ .try_into()
+ .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()
.user_agent(user_agent)
.default_headers(headers)
diff --git a/tests/ci_test.rs b/tests/ci_test.rs
index e5cb1c2..3d98f71 100644
--- a/tests/ci_test.rs
+++ b/tests/ci_test.rs
@@ -5,7 +5,7 @@ use forgejo_api::Forgejo;
async fn ci() -> eyre::Result<()> {
let url = url::Url::parse(&std::env::var("FORGEJO_API_CI_INSTANCE_URL")?)?;
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();
@@ -54,6 +54,22 @@ async fn user(api: &forgejo_api::Forgejo) -> eyre::Result<()> {
let followers = api.get_followers("TestingAdmin").await?;
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(())
}