diff options
author | Cyborus <cyborus@noreply.codeberg.org> | 2023-12-20 20:58:22 +0100 |
---|---|---|
committer | Cyborus <cyborus@noreply.codeberg.org> | 2023-12-20 20:58:22 +0100 |
commit | c220b8429b4180e25efc81e2cdef1e7526ffd272 (patch) | |
tree | dbefca01ba5117c11b93458c95b1ddd84a4c646b | |
parent | Merge pull request 'don't use rustls' (#31) from no-rustls into main (diff) | |
parent | add password auth testing (diff) | |
download | forgejo-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.lock | 14 | ||||
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | src/lib.rs | 86 | ||||
-rw-r--r-- | tests/ci_test.rs | 18 |
4 files changed, 107 insertions, 13 deletions
@@ -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" @@ -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" @@ -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(()) } |