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
					
				
					 4 changed files with 107 additions and 13 deletions
				
			
		
							
								
								
									
										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"
 | 
			
		||||
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"
 | 
			
		||||
| 
						 | 
				
			
			@ -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"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										86
									
								
								src/lib.rs
									
										
									
									
									
								
							
							
						
						
									
										86
									
								
								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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue