diff --git a/Cargo.lock b/Cargo.lock index e0180a9..c8df65c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -699,6 +699,7 @@ dependencies = [ "rand", "reqwest", "rsa", + "secrecy", "semver", "serde", "serde_json", @@ -730,6 +731,7 @@ dependencies = [ "rand", "reqwest", "rsa", + "secrecy", "semver", "serde", "serde_json", @@ -1713,6 +1715,16 @@ dependencies = [ ] [[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "serde", + "zeroize", +] + +[[package]] name = "security-framework" version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/giterated-daemon/Cargo.toml b/giterated-daemon/Cargo.toml index 561bc39..ada4ec8 100644 --- a/giterated-daemon/Cargo.toml +++ b/giterated-daemon/Cargo.toml @@ -38,5 +38,6 @@ git2 = "0.17" thiserror = "1" anyhow = "1" sqlx = { version = "0.7", features = [ "runtime-tokio", "tls-native-tls", "postgres", "macros", "migrate", "chrono" ] } +secrecy = "0.8.0" #uuid = { version = "1.4", features = [ "v4", "serde" ] } diff --git a/giterated-daemon/src/authentication.rs b/giterated-daemon/src/authentication.rs index 0476306..88caab9 100644 --- a/giterated-daemon/src/authentication.rs +++ b/giterated-daemon/src/authentication.rs @@ -1,11 +1,8 @@ use anyhow::Error; -use giterated_models::{ - messages::authentication::{AuthenticationTokenResponse, TokenExtensionResponse}, - model::{ - authenticated::{UserAuthenticationToken, UserTokenMetadata}, - instance::Instance, - user::User, - }, +use giterated_models::model::{ + authenticated::{UserAuthenticationToken, UserTokenMetadata}, + instance::Instance, + user::User, }; use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, TokenData, Validation}; use std::{sync::Arc, time::SystemTime}; @@ -21,9 +18,6 @@ pub struct AuthenticationTokenGranter { impl AuthenticationTokenGranter { async fn private_key(&self) -> Vec { - let _secret_key = self.config["authentication"]["secret_key"] - .as_str() - .unwrap(); let mut file = File::open( self.config["giterated"]["keys"]["private"] .as_str() @@ -68,7 +62,7 @@ impl AuthenticationTokenGranter { issued_for: impl ToOwned, username: String, _password: String, - ) -> Result { + ) -> Result { let private_key = { let mut file = File::open( self.config["giterated"]["keys"]["private"] @@ -104,9 +98,7 @@ impl AuthenticationTokenGranter { ) .unwrap(); - Ok(AuthenticationTokenResponse { - token: UserAuthenticationToken::from(token), - }) + Ok(UserAuthenticationToken::from(token)) } pub async fn extension_request( @@ -114,7 +106,7 @@ impl AuthenticationTokenGranter { issued_for: &Instance, key_cache: &Arc>, token: UserAuthenticationToken, - ) -> Result { + ) -> Result, Error> { let mut key_cache = key_cache.lock().await; let server_public_key = key_cache.get(issued_for).await?; drop(key_cache); @@ -167,8 +159,6 @@ impl AuthenticationTokenGranter { ) .unwrap(); - Ok(TokenExtensionResponse { - new_token: Some(token), - }) + Ok(Some(UserAuthenticationToken::from(token))) } } diff --git a/giterated-daemon/src/backend/git.rs b/giterated-daemon/src/backend/git.rs index 6f1d9f3..3ae0b3a 100644 --- a/giterated-daemon/src/backend/git.rs +++ b/giterated-daemon/src/backend/git.rs @@ -1,22 +1,23 @@ use anyhow::Error; use async_trait::async_trait; use futures_util::StreamExt; -use git2::ObjectType; + use giterated_models::{ - messages::repository::{ - RepositoryCreateRequest, RepositoryCreateResponse, RepositoryFileInspectRequest, - RepositoryFileInspectionResponse, RepositoryInfoRequest, RepositoryIssueLabelsRequest, - RepositoryIssueLabelsResponse, RepositoryIssuesCountRequest, RepositoryIssuesCountResponse, - RepositoryIssuesRequest, RepositoryIssuesResponse, - }, model::{ instance::Instance, repository::{ - Commit, Repository, RepositoryObjectType, RepositorySummary, RepositoryTreeEntry, - RepositoryView, RepositoryVisibility, + Commit, IssueLabel, Repository, RepositoryIssue, RepositorySummary, + RepositoryTreeEntry, RepositoryVisibility, }, user::User, }, + operation::{ + instance::RepositoryCreateRequest, + repository::{ + RepositoryFileInspectRequest, RepositoryIssueLabelsRequest, + RepositoryIssuesCountRequest, RepositoryIssuesRequest, + }, + }, }; use sqlx::{Either, PgPool}; use std::path::{Path, PathBuf}; @@ -208,11 +209,21 @@ impl GitBackend { #[async_trait] impl RepositoryBackend for GitBackend { + async fn exists(&mut self, repository: &Repository) -> Result { + if let Ok(_repository) = self + .find_by_owner_user_name(&repository.owner, &repository.name) + .await + { + return Ok(true); + } else { + return Ok(false); + } + } async fn create_repository( &mut self, _user: &User, request: &RepositoryCreateRequest, - ) -> Result { + ) -> Result { // Check if repository already exists in the database if let Ok(repository) = self .find_by_owner_user_name(&request.owner, &request.name) @@ -255,7 +266,11 @@ impl RepositoryBackend for GitBackend { "Created new repository with the name {}/{}/{}", request.owner.instance.url, request.owner.username, request.name ); - Ok(RepositoryCreateResponse) + Ok(Repository { + owner: request.owner.clone(), + name: request.name.clone(), + instance: request.instance.as_ref().unwrap_or(&self.instance).clone(), + }) } Err(err) => { let err = GitBackendError::FailedCreatingRepository(err); @@ -271,171 +286,171 @@ impl RepositoryBackend for GitBackend { } } - async fn repository_info( - &mut self, - requester: Option<&User>, - request: &RepositoryInfoRequest, - ) -> Result { - let repository = match self - .find_by_owner_user_name( - // &request.owner.instance.url, - &request.repository.owner, - &request.repository.name, - ) - .await - { - Ok(repository) => repository, - Err(err) => return Err(Box::new(err).into()), - }; - - if let Some(requester) = requester { - if !repository.can_user_view_repository(Some(&requester)) { - return Err(Box::new(GitBackendError::RepositoryNotFound { - owner_user: request.repository.owner.to_string(), - name: request.repository.name.clone(), - }) - .into()); - } - } else if matches!(repository.visibility, RepositoryVisibility::Private) { - info!("Unauthenticated"); - // Unauthenticated users can never view private repositories - - return Err(Box::new(GitBackendError::RepositoryNotFound { - owner_user: request.repository.owner.to_string(), - name: request.repository.name.clone(), - }) - .into()); - } - - let git = match repository.open_git2_repository(&self.repository_folder) { - Ok(git) => git, - Err(err) => return Err(Box::new(err).into()), - }; - - let rev_name = match &request.rev { - None => { - if let Ok(head) = git.head() { - head.name().unwrap().to_string() - } else { - // Nothing in database, render empty tree. - return Ok(RepositoryView { - name: repository.name, - owner: request.repository.owner.clone(), - description: repository.description, - visibility: repository.visibility, - default_branch: repository.default_branch, - latest_commit: None, - tree_rev: None, - tree: vec![], - }); - } - } - Some(rev_name) => { - // Find the reference, otherwise return GitBackendError - match git - .find_reference(format!("refs/heads/{}", rev_name).as_str()) - .map_err(|_| GitBackendError::RefNotFound(rev_name.to_string())) - { - Ok(reference) => reference.name().unwrap().to_string(), - Err(err) => return Err(Box::new(err).into()), - } - } - }; - - // Get the git object as a commit - let rev = match git - .revparse_single(rev_name.as_str()) - .map_err(|_| GitBackendError::RefNotFound(rev_name.to_string())) - { - Ok(rev) => rev, - Err(err) => return Err(Box::new(err).into()), - }; - let commit = rev.as_commit().unwrap(); - - // this is stupid - let mut current_path = rev_name.replace("refs/heads/", ""); - - // Get the commit tree - let git_tree = if let Some(path) = &request.path { - // Add it to our full path string - current_path.push_str(format!("/{}", path).as_str()); - // Get the specified path, return an error if it wasn't found. - let entry = match commit - .tree() - .unwrap() - .get_path(&PathBuf::from(path)) - .map_err(|_| GitBackendError::PathNotFound(path.to_string())) - { - Ok(entry) => entry, - Err(err) => return Err(Box::new(err).into()), - }; - // Turn the entry into a git tree - entry.to_object(&git).unwrap().as_tree().unwrap().clone() - } else { - commit.tree().unwrap() - }; - - // Iterate over the git tree and collect it into our own tree types - let mut tree = git_tree - .iter() - .map(|entry| { - let object_type = match entry.kind().unwrap() { - ObjectType::Tree => RepositoryObjectType::Tree, - ObjectType::Blob => RepositoryObjectType::Blob, - _ => unreachable!(), - }; - let mut tree_entry = - RepositoryTreeEntry::new(entry.name().unwrap(), object_type, entry.filemode()); - - if request.extra_metadata { - // Get the file size if It's a blob - let object = entry.to_object(&git).unwrap(); - if let Some(blob) = object.as_blob() { - tree_entry.size = Some(blob.size()); - } - - // Could possibly be done better - let path = if let Some(path) = current_path.split_once('/') { - format!("{}/{}", path.1, entry.name().unwrap()) - } else { - entry.name().unwrap().to_string() - }; - - // Get the last commit made to the entry - if let Ok(last_commit) = - GitBackend::get_last_commit_of_file(&path, &git, commit) - { - tree_entry.last_commit = Some(last_commit); - } - } - - tree_entry - }) - .collect::>(); - - // Sort the tree alphabetically and with tree first - tree.sort_unstable_by_key(|entry| entry.name.to_lowercase()); - tree.sort_unstable_by_key(|entry| { - std::cmp::Reverse(format!("{:?}", entry.object_type).to_lowercase()) - }); - - Ok(RepositoryView { - name: repository.name, - owner: request.repository.owner.clone(), - description: repository.description, - visibility: repository.visibility, - default_branch: repository.default_branch, - latest_commit: Some(Commit::from(commit.clone())), - tree_rev: Some(rev_name), - tree, - }) - } + // async fn repository_info( + // &mut self, + // requester: Option<&User>, + // request: &RepositoryInfoRequest, + // ) -> Result { + // let repository = match self + // .find_by_owner_user_name( + // // &request.owner.instance.url, + // &request.repository.owner, + // &request.repository.name, + // ) + // .await + // { + // Ok(repository) => repository, + // Err(err) => return Err(Box::new(err).into()), + // }; + + // if let Some(requester) = requester { + // if !repository.can_user_view_repository(Some(&requester)) { + // return Err(Box::new(GitBackendError::RepositoryNotFound { + // owner_user: request.repository.owner.to_string(), + // name: request.repository.name.clone(), + // }) + // .into()); + // } + // } else if matches!(repository.visibility, RepositoryVisibility::Private) { + // info!("Unauthenticated"); + // // Unauthenticated users can never view private repositories + + // return Err(Box::new(GitBackendError::RepositoryNotFound { + // owner_user: request.repository.owner.to_string(), + // name: request.repository.name.clone(), + // }) + // .into()); + // } + + // let git = match repository.open_git2_repository(&self.repository_folder) { + // Ok(git) => git, + // Err(err) => return Err(Box::new(err).into()), + // }; + + // let rev_name = match &request.rev { + // None => { + // if let Ok(head) = git.head() { + // head.name().unwrap().to_string() + // } else { + // // Nothing in database, render empty tree. + // return Ok(RepositoryView { + // name: repository.name, + // owner: request.repository.owner.clone(), + // description: repository.description, + // visibility: repository.visibility, + // default_branch: repository.default_branch, + // latest_commit: None, + // tree_rev: None, + // tree: vec![], + // }); + // } + // } + // Some(rev_name) => { + // // Find the reference, otherwise return GitBackendError + // match git + // .find_reference(format!("refs/heads/{}", rev_name).as_str()) + // .map_err(|_| GitBackendError::RefNotFound(rev_name.to_string())) + // { + // Ok(reference) => reference.name().unwrap().to_string(), + // Err(err) => return Err(Box::new(err).into()), + // } + // } + // }; + + // // Get the git object as a commit + // let rev = match git + // .revparse_single(rev_name.as_str()) + // .map_err(|_| GitBackendError::RefNotFound(rev_name.to_string())) + // { + // Ok(rev) => rev, + // Err(err) => return Err(Box::new(err).into()), + // }; + // let commit = rev.as_commit().unwrap(); + + // // this is stupid + // let mut current_path = rev_name.replace("refs/heads/", ""); + + // // Get the commit tree + // let git_tree = if let Some(path) = &request.path { + // // Add it to our full path string + // current_path.push_str(format!("/{}", path).as_str()); + // // Get the specified path, return an error if it wasn't found. + // let entry = match commit + // .tree() + // .unwrap() + // .get_path(&PathBuf::from(path)) + // .map_err(|_| GitBackendError::PathNotFound(path.to_string())) + // { + // Ok(entry) => entry, + // Err(err) => return Err(Box::new(err).into()), + // }; + // // Turn the entry into a git tree + // entry.to_object(&git).unwrap().as_tree().unwrap().clone() + // } else { + // commit.tree().unwrap() + // }; + + // // Iterate over the git tree and collect it into our own tree types + // let mut tree = git_tree + // .iter() + // .map(|entry| { + // let object_type = match entry.kind().unwrap() { + // ObjectType::Tree => RepositoryObjectType::Tree, + // ObjectType::Blob => RepositoryObjectType::Blob, + // _ => unreachable!(), + // }; + // let mut tree_entry = + // RepositoryTreeEntry::new(entry.name().unwrap(), object_type, entry.filemode()); + + // if request.extra_metadata { + // // Get the file size if It's a blob + // let object = entry.to_object(&git).unwrap(); + // if let Some(blob) = object.as_blob() { + // tree_entry.size = Some(blob.size()); + // } + + // // Could possibly be done better + // let path = if let Some(path) = current_path.split_once('/') { + // format!("{}/{}", path.1, entry.name().unwrap()) + // } else { + // entry.name().unwrap().to_string() + // }; + + // // Get the last commit made to the entry + // if let Ok(last_commit) = + // GitBackend::get_last_commit_of_file(&path, &git, commit) + // { + // tree_entry.last_commit = Some(last_commit); + // } + // } + + // tree_entry + // }) + // .collect::>(); + + // // Sort the tree alphabetically and with tree first + // tree.sort_unstable_by_key(|entry| entry.name.to_lowercase()); + // tree.sort_unstable_by_key(|entry| { + // std::cmp::Reverse(format!("{:?}", entry.object_type).to_lowercase()) + // }); + + // Ok(RepositoryView { + // name: repository.name, + // owner: request.repository.owner.clone(), + // description: repository.description, + // visibility: repository.visibility, + // default_branch: repository.default_branch, + // latest_commit: Some(Commit::from(commit.clone())), + // tree_rev: Some(rev_name), + // tree, + // }) + // } async fn repository_file_inspect( &mut self, _requester: Option<&User>, _request: &RepositoryFileInspectRequest, - ) -> Result { + ) -> Result, Error> { todo!() } @@ -484,7 +499,7 @@ impl IssuesBackend for GitBackend { &mut self, _requester: Option<&User>, _request: &RepositoryIssuesCountRequest, - ) -> Result { + ) -> Result { todo!() } @@ -492,7 +507,7 @@ impl IssuesBackend for GitBackend { &mut self, _requester: Option<&User>, _request: &RepositoryIssueLabelsRequest, - ) -> Result { + ) -> Result, Error> { todo!() } @@ -500,7 +515,7 @@ impl IssuesBackend for GitBackend { &mut self, _requester: Option<&User>, _request: &RepositoryIssuesRequest, - ) -> Result { + ) -> Result, Error> { todo!() } } diff --git a/giterated-daemon/src/backend/mod.rs b/giterated-daemon/src/backend/mod.rs index 6fa9eb8..d5aaaeb 100644 --- a/giterated-daemon/src/backend/mod.rs +++ b/giterated-daemon/src/backend/mod.rs @@ -10,27 +10,21 @@ use serde_json::Value; use crate::backend::git::GitBackendError; use giterated_models::{ - messages::{ - authentication::{ - AuthenticationTokenRequest, AuthenticationTokenResponse, RegisterAccountRequest, - RegisterAccountResponse, - }, - repository::{ - RepositoryCreateRequest, RepositoryCreateResponse, RepositoryFileInspectRequest, - RepositoryFileInspectionResponse, RepositoryInfoRequest, RepositoryIssueLabelsRequest, - RepositoryIssueLabelsResponse, RepositoryIssuesCountRequest, - RepositoryIssuesCountResponse, RepositoryIssuesRequest, RepositoryIssuesResponse, - }, - user::{ - UserBioRequest, UserBioResponse, UserDisplayImageRequest, UserDisplayImageResponse, - UserDisplayNameRequest, UserDisplayNameResponse, - }, - }, model::{ + authenticated::UserAuthenticationToken, instance::Instance, - repository::{Repository, RepositorySummary, RepositoryView}, + repository::{ + IssueLabel, Repository, RepositoryIssue, RepositorySummary, RepositoryTreeEntry, + }, user::User, }, + operation::{ + instance::{AuthenticationTokenRequest, RegisterAccountRequest, RepositoryCreateRequest}, + repository::{ + RepositoryFileInspectRequest, RepositoryIssueLabelsRequest, + RepositoryIssuesCountRequest, RepositoryIssuesRequest, + }, + }, }; #[async_trait] @@ -39,22 +33,18 @@ pub trait RepositoryBackend: IssuesBackend { &mut self, user: &User, request: &RepositoryCreateRequest, - ) -> Result; - async fn repository_info( - &mut self, - requester: Option<&User>, - request: &RepositoryInfoRequest, - ) -> Result; + ) -> Result; async fn repository_file_inspect( &mut self, requester: Option<&User>, request: &RepositoryFileInspectRequest, - ) -> Result; + ) -> Result, Error>; async fn repositories_for_user( &mut self, requester: Option<&User>, user: &User, ) -> Result, Error>; + async fn exists(&mut self, repository: &Repository) -> Result; } pub trait IssuesBackend { @@ -62,17 +52,17 @@ pub trait IssuesBackend { &mut self, requester: Option<&User>, request: &RepositoryIssuesCountRequest, - ) -> Result; + ) -> Result; fn issue_labels( &mut self, requester: Option<&User>, request: &RepositoryIssueLabelsRequest, - ) -> Result; + ) -> Result, Error>; fn issues( &mut self, requester: Option<&User>, request: &RepositoryIssuesRequest, - ) -> Result; + ) -> Result, Error>; } #[async_trait::async_trait] @@ -80,28 +70,17 @@ pub trait AuthBackend { async fn register( &mut self, request: RegisterAccountRequest, - ) -> Result; + ) -> Result; async fn login( &mut self, source: &Instance, request: AuthenticationTokenRequest, - ) -> Result; + ) -> Result; } #[async_trait::async_trait] pub trait UserBackend: AuthBackend { - async fn display_name( - &mut self, - request: UserDisplayNameRequest, - ) -> Result; - - async fn display_image( - &mut self, - request: UserDisplayImageRequest, - ) -> Result; - - async fn bio(&mut self, request: UserBioRequest) -> Result; async fn exists(&mut self, user: &User) -> Result; } diff --git a/giterated-daemon/src/backend/user.rs b/giterated-daemon/src/backend/user.rs index 803c4e1..cdb0d16 100644 --- a/giterated-daemon/src/backend/user.rs +++ b/giterated-daemon/src/backend/user.rs @@ -6,22 +6,8 @@ use aes_gcm::{aead::Aead, AeadCore, Aes256Gcm, Key, KeyInit}; use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; use base64::{engine::general_purpose::STANDARD, Engine as _}; use giterated_models::{ - messages::{ - authentication::{ - AuthenticationTokenRequest, AuthenticationTokenResponse, RegisterAccountRequest, - RegisterAccountResponse, - }, - user::{ - UserBioRequest, UserBioResponse, UserDisplayImageRequest, UserDisplayImageResponse, - UserDisplayNameRequest, UserDisplayNameResponse, - }, - }, - model::{ - authenticated::UserAuthenticationToken, - instance::Instance, - settings::{Setting, UserBio, UserDisplayImage, UserDisplayName}, - user::User, - }, + model::{authenticated::UserAuthenticationToken, instance::Instance, user::User}, + operation::instance::{AuthenticationTokenRequest, RegisterAccountRequest}, }; use rsa::{ pkcs8::{EncodePrivateKey, EncodePublicKey}, @@ -29,6 +15,7 @@ use rsa::{ RsaPrivateKey, RsaPublicKey, }; +use secrecy::ExposeSecret; use sqlx::PgPool; use tokio::sync::Mutex; @@ -61,71 +48,6 @@ impl UserAuth { #[async_trait::async_trait] impl UserBackend for UserAuth { - async fn display_name( - &mut self, - request: UserDisplayNameRequest, - ) -> Result { - let mut settings_backend = self.settings_provider.lock().await; - let settings = settings_backend.user_get(&request.user).await?; - drop(settings_backend); - - let name = settings - .iter() - .find(|setting| &setting.0 == UserDisplayName::name()); - - if let Some((_, name)) = name { - let name: UserDisplayName = serde_json::from_value(name.clone()).unwrap(); - - Ok(UserDisplayNameResponse { - display_name: Some(name.0), - }) - } else { - Ok(UserDisplayNameResponse { display_name: None }) - } - } - - async fn display_image( - &mut self, - request: UserDisplayImageRequest, - ) -> Result { - let mut settings_backend: tokio::sync::MutexGuard<'_, dyn SettingsBackend> = - self.settings_provider.lock().await; - let settings = settings_backend.user_get(&request.user).await?; - drop(settings_backend); - - let image = settings - .iter() - .find(|setting| &setting.0 == UserDisplayImage::name()); - - if let Some((_, image)) = image { - let image: UserDisplayImage = serde_json::from_value(image.clone()).unwrap(); - - Ok(UserDisplayImageResponse { - image_url: Some(image.0), - }) - } else { - Ok(UserDisplayImageResponse { image_url: None }) - } - } - - async fn bio(&mut self, request: UserBioRequest) -> Result { - let mut settings_backend = self.settings_provider.lock().await; - let settings = settings_backend.user_get(&request.user).await?; - drop(settings_backend); - - let bio = settings - .iter() - .find(|setting| &setting.0 == UserBio::name()); - - if let Some((_, bio)) = bio { - let bio: UserBio = serde_json::from_value(bio.clone()).unwrap(); - - Ok(UserBioResponse { bio: Some(bio.0) }) - } else { - Ok(UserBioResponse { bio: None }) - } - } - async fn exists(&mut self, user: &User) -> Result { Ok(sqlx::query_as!( UserRow, @@ -134,7 +56,7 @@ impl UserBackend for UserAuth { ) .fetch_one(&self.pg_pool.clone()) .await - .is_err()) + .is_ok()) } } @@ -143,7 +65,7 @@ impl AuthBackend for UserAuth { async fn register( &mut self, request: RegisterAccountRequest, - ) -> Result { + ) -> Result { const BITS: usize = 2048; let private_key = RsaPrivateKey::new(&mut OsRng, BITS).unwrap(); @@ -153,13 +75,13 @@ impl AuthBackend for UserAuth { let mut target: [u8; 32] = [0; 32]; let mut index = 0; - let mut iterator = request.password.as_bytes().iter(); + let mut iterator = request.password.expose_secret().0.as_bytes().iter(); while index < 32 { if let Some(next) = iterator.next() { target[index] = *next; index += 1; } else { - iterator = request.password.as_bytes().iter(); + iterator = request.password.expose_secret().0.as_bytes().iter(); } } @@ -180,7 +102,7 @@ impl AuthBackend for UserAuth { let argon2 = Argon2::default(); let password_hash = argon2 - .hash_password(request.password.as_bytes(), &salt) + .hash_password(request.password.expose_secret().0.as_bytes(), &salt) .unwrap() .to_string(); @@ -217,14 +139,15 @@ impl AuthBackend for UserAuth { ) .await; - Ok(RegisterAccountResponse { token }) + Ok(UserAuthenticationToken::from(token)) } async fn login( &mut self, source: &Instance, request: AuthenticationTokenRequest, - ) -> Result { + ) -> Result { + info!("fetching!"); let user = sqlx::query_as!( UserRow, r#"SELECT * FROM users WHERE username = $1"#, @@ -235,11 +158,11 @@ impl AuthBackend for UserAuth { let hash = PasswordHash::new(&user.password).unwrap(); - if !matches!( - Argon2::default().verify_password(request.password.as_bytes(), &hash), - Ok(()) - ) { - // Invalid password! + if Argon2::default() + .verify_password(request.password.expose_secret().0.as_bytes(), &hash) + .is_err() + { + info!("invalid password"); return Err(Error::from(AuthenticationError::InvalidPassword)); } @@ -254,9 +177,7 @@ impl AuthBackend for UserAuth { ) .await; - Ok(AuthenticationTokenResponse { - token: UserAuthenticationToken::from(token), - }) + Ok(UserAuthenticationToken::from(token)) } } diff --git a/giterated-daemon/src/connection.rs b/giterated-daemon/src/connection.rs index 2d4cd51..2e43434 100644 --- a/giterated-daemon/src/connection.rs +++ b/giterated-daemon/src/connection.rs @@ -1,25 +1,20 @@ -pub mod authentication; -pub mod forwarded; -pub mod handshake; -pub mod repository; -pub mod user; +// pub mod authentication; +// pub mod forwarded; +// pub mod handshake; +// pub mod repository; +// pub mod user; pub mod wrapper; use std::{any::type_name, collections::HashMap}; use anyhow::Error; -use giterated_models::{ - messages::ErrorMessage, - model::instance::{Instance, InstanceMeta}, -}; +use giterated_models::model::instance::{Instance, InstanceMeta}; use serde::{de::DeserializeOwned, Serialize}; use tokio::{net::TcpStream, task::JoinHandle}; use tokio_tungstenite::WebSocketStream; #[derive(Debug, thiserror::Error)] pub enum ConnectionError { - #[error("connection error message {0}")] - ErrorMessage(#[from] ErrorMessage), #[error("connection should close")] Shutdown, #[error("internal error {0}")] diff --git a/giterated-daemon/src/connection/wrapper.rs b/giterated-daemon/src/connection/wrapper.rs index c1f97ea..866d890 100644 --- a/giterated-daemon/src/connection/wrapper.rs +++ b/giterated-daemon/src/connection/wrapper.rs @@ -1,17 +1,11 @@ use std::{ net::SocketAddr, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, + sync::{atomic::AtomicBool, Arc}, }; use anyhow::Error; use futures_util::{SinkExt, StreamExt}; -use giterated_models::{ - messages::error::ConnectionError, - model::{authenticated::AuthenticatedPayload, instance::Instance}, -}; +use giterated_models::model::instance::Instance; use serde::Serialize; @@ -22,16 +16,11 @@ use toml::Table; use crate::{ authentication::AuthenticationTokenGranter, backend::{RepositoryBackend, SettingsBackend, UserBackend}, - connection::forwarded::wrap_forwarded, federation::connections::InstanceConnections, keys::PublicKeyCache, - message::NetworkMessage, }; -use super::{ - authentication::authentication_handle, handshake::handshake_handle, - repository::repository_handle, user::user_handle, Connections, -}; +use super::Connections; pub async fn connection_wrapper( socket: WebSocketStream, @@ -45,7 +34,7 @@ pub async fn connection_wrapper( instance_connections: Arc>, config: Table, ) { - let connection_state = ConnectionState { + let _connection_state = ConnectionState { socket: Arc::new(Mutex::new(socket)), connections, repository_backend, @@ -60,118 +49,118 @@ pub async fn connection_wrapper( config, }; - let mut handshaked = false; - - loop { - let mut socket = connection_state.socket.lock().await; - let message = socket.next().await; - drop(socket); - - match message { - Some(Ok(message)) => { - let payload = match message { - Message::Binary(payload) => payload, - Message::Ping(_) => { - let mut socket = connection_state.socket.lock().await; - let _ = socket.send(Message::Pong(vec![])).await; - drop(socket); - continue; - } - Message::Close(_) => return, - _ => continue, - }; - info!("one payload"); - - let message = NetworkMessage(payload.clone()); - - if !handshaked { - info!("im foo baring"); - if handshake_handle(&message, &connection_state).await.is_ok() { - if connection_state.handshaked.load(Ordering::SeqCst) { - handshaked = true; - } - } - } else { - let raw = serde_json::from_slice::(&payload).unwrap(); - - if let Some(target_instance) = &raw.target_instance { - if connection_state.instance != *target_instance { - // Forward request - info!("Forwarding message to {}", target_instance.url); - let mut instance_connections = instance_connections.lock().await; - let pool = instance_connections.get_or_open(&target_instance).unwrap(); - let pool_clone = pool.clone(); - drop(pool); - - let result = wrap_forwarded(&pool_clone, raw).await; - - let mut socket = connection_state.socket.lock().await; - let _ = socket.send(result).await; - - continue; - } - } - - let message_type = &raw.message_type; - - info!("Handling message with type: {}", message_type); - - match authentication_handle(message_type, &message, &connection_state).await { - Err(e) => { - let _ = connection_state - .send_raw(ConnectionError(e.to_string())) - .await; - } - Ok(true) => continue, - Ok(false) => {} - } - - match repository_handle(message_type, &message, &connection_state).await { - Err(e) => { - let _ = connection_state - .send_raw(ConnectionError(e.to_string())) - .await; - } - Ok(true) => continue, - Ok(false) => {} - } - - match user_handle(message_type, &message, &connection_state).await { - Err(e) => { - let _ = connection_state - .send_raw(ConnectionError(e.to_string())) - .await; - } - Ok(true) => continue, - Ok(false) => {} - } - - match authentication_handle(message_type, &message, &connection_state).await { - Err(e) => { - let _ = connection_state - .send_raw(ConnectionError(e.to_string())) - .await; - } - Ok(true) => continue, - Ok(false) => {} - } - - error!( - "Message completely unhandled: {}", - std::str::from_utf8(&payload).unwrap() - ); - } - } - Some(Err(e)) => { - error!("Closing connection for {:?} for {}", e, addr); - return; - } - _ => { - info!("Unhandled"); - continue; - } - } - } + let _handshaked = false; + + // loop { + // let mut socket = connection_state.socket.lock().await; + // let message = socket.next().await; + // drop(socket); + + // match message { + // Some(Ok(message)) => { + // let payload = match message { + // Message::Binary(payload) => payload, + // Message::Ping(_) => { + // let mut socket = connection_state.socket.lock().await; + // let _ = socket.send(Message::Pong(vec![])).await; + // drop(socket); + // continue; + // } + // Message::Close(_) => return, + // _ => continue, + // }; + // info!("one payload"); + + // let message = NetworkMessage(payload.clone()); + + // if !handshaked { + // info!("im foo baring"); + // if handshake_handle(&message, &connection_state).await.is_ok() { + // if connection_state.handshaked.load(Ordering::SeqCst) { + // handshaked = true; + // } + // } + // } else { + // let raw = serde_json::from_slice::(&payload).unwrap(); + + // if let Some(target_instance) = &raw.target_instance { + // if connection_state.instance != *target_instance { + // // Forward request + // info!("Forwarding message to {}", target_instance.url); + // let mut instance_connections = instance_connections.lock().await; + // let pool = instance_connections.get_or_open(&target_instance).unwrap(); + // let pool_clone = pool.clone(); + // drop(pool); + + // let result = wrap_forwarded(&pool_clone, raw).await; + + // let mut socket = connection_state.socket.lock().await; + // let _ = socket.send(result).await; + + // continue; + // } + // } + + // let message_type = &raw.message_type; + + // info!("Handling message with type: {}", message_type); + + // match authentication_handle(message_type, &message, &connection_state).await { + // Err(e) => { + // let _ = connection_state + // .send_raw(ConnectionError(e.to_string())) + // .await; + // } + // Ok(true) => continue, + // Ok(false) => {} + // } + + // match repository_handle(message_type, &message, &connection_state).await { + // Err(e) => { + // let _ = connection_state + // .send_raw(ConnectionError(e.to_string())) + // .await; + // } + // Ok(true) => continue, + // Ok(false) => {} + // } + + // match user_handle(message_type, &message, &connection_state).await { + // Err(e) => { + // let _ = connection_state + // .send_raw(ConnectionError(e.to_string())) + // .await; + // } + // Ok(true) => continue, + // Ok(false) => {} + // } + + // match authentication_handle(message_type, &message, &connection_state).await { + // Err(e) => { + // let _ = connection_state + // .send_raw(ConnectionError(e.to_string())) + // .await; + // } + // Ok(true) => continue, + // Ok(false) => {} + // } + + // error!( + // "Message completely unhandled: {}", + // std::str::from_utf8(&payload).unwrap() + // ); + // } + // } + // Some(Err(e)) => { + // error!("Closing connection for {:?} for {}", e, addr); + // return; + // } + // _ => { + // info!("Unhandled"); + // continue; + // } + // } + // } } #[derive(Clone)] diff --git a/giterated-daemon/src/database_backend/mod.rs b/giterated-daemon/src/database_backend/mod.rs new file mode 100644 index 0000000..3fc61e8 --- /dev/null +++ b/giterated-daemon/src/database_backend/mod.rs @@ -0,0 +1,109 @@ +use std::{str::FromStr, sync::Arc}; + +use giterated_models::{ + error::OperationError, + model::{instance::Instance, repository::Repository, user::User}, + operation::{GiteratedObject, GiteratedOperation, Object, ObjectBackend, ObjectRequestError}, +}; +use sqlx::PgPool; +use std::fmt::Debug; +use tokio::sync::Mutex; + +use crate::backend::{RepositoryBackend, UserBackend}; + +#[derive(Clone, Debug)] +pub struct Foobackend {} + +#[async_trait::async_trait] +impl ObjectBackend for Foobackend { + async fn object_operation + Debug>( + &self, + _object: O, + _operation: D, + ) -> Result> { + // We don't handle operations with this backend + Err(OperationError::Unhandled) + } + + async fn get_object( + &self, + _object_str: &str, + ) -> Result, OperationError> { + todo!() + } +} + +/// A backend implementation which attempts to resolve data from the instance's database. +#[derive(Clone)] +pub struct DatabaseBackend<'b> { + our_instance: Object<'b, Instance, Foobackend>, + user_backend: Arc>, + repository_backend: Arc>, + pool: PgPool, +} + +impl Debug for DatabaseBackend<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DatabaseBackend").finish() + } +} + +#[async_trait::async_trait] +impl<'b> ObjectBackend for DatabaseBackend<'b> { + async fn object_operation + Debug>( + &self, + _object: O, + _operation: D, + ) -> Result> { + // We don't handle operations with this backend + Err(OperationError::Unhandled) + } + + async fn get_object( + &self, + object_str: &str, + ) -> Result, OperationError> { + if let Ok(user) = User::from_str(object_str) { + let mut user_backend = self.user_backend.lock().await; + + if user_backend + .exists(&user) + .await + .map_err(|e| OperationError::Internal(e.to_string()))? + { + Ok(unsafe { + Object::new_unchecked( + O::from_object_str(object_str) + .map_err(|e| ObjectRequestError::Deserialization(e.to_string()))?, + self.clone(), + ) + }) + } else { + return Err(OperationError::Unhandled); + } + } else if let Ok(repository) = Repository::from_str(object_str) { + let mut repository_backend = self.repository_backend.lock().await; + + if repository_backend + .exists(&repository) + .await + .map_err(|e| OperationError::Internal(e.to_string()))? + { + Ok(unsafe { + Object::new_unchecked( + O::from_object_str(object_str) + .map_err(|e| ObjectRequestError::Deserialization(e.to_string()))?, + self.clone(), + ) + }) + } else { + return Err(OperationError::Unhandled); + } + } else if Instance::from_str(object_str).is_ok() { + return Err(OperationError::Unhandled); + } else { + // Invalid object type + return Err(OperationError::Unhandled); + } + } +} diff --git a/giterated-daemon/src/lib.rs b/giterated-daemon/src/lib.rs index 592deb1..e295cdc 100644 --- a/giterated-daemon/src/lib.rs +++ b/giterated-daemon/src/lib.rs @@ -5,6 +5,7 @@ use semver::{Version, VersionReq}; pub mod authentication; pub mod backend; pub mod connection; +pub mod database_backend; pub mod federation; pub mod keys; pub mod message; diff --git a/giterated-models/Cargo.toml b/giterated-models/Cargo.toml index 11d80f7..75674de 100644 --- a/giterated-models/Cargo.toml +++ b/giterated-models/Cargo.toml @@ -24,6 +24,7 @@ aes-gcm = "0.10.2" semver = {version = "*", features = ["serde"]} tower = "*" bincode = "*" +secrecy = { version = "0.8.0", features = ["serde"] } toml = { version = "0.7" } diff --git a/giterated-models/src/error.rs b/giterated-models/src/error.rs new file mode 100644 index 0000000..62f6c71 --- /dev/null +++ b/giterated-models/src/error.rs @@ -0,0 +1,28 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, thiserror::Error, Deserialize, Serialize)] +pub enum InstanceError { + #[error("registration failed")] + RegistrationFailure, + #[error("authentication failed")] + AuthenticationFailed, +} + +#[derive(Debug, thiserror::Error, Serialize, Deserialize)] +pub enum RepositoryError {} + +#[derive(Debug, thiserror::Error, Deserialize, Serialize)] +pub enum UserError {} + +#[derive(Debug, thiserror::Error, Serialize, Deserialize)] +pub enum GetValueError {} + +#[derive(Serialize, Deserialize, Debug, thiserror::Error)] +pub enum OperationError { + #[error("the operation was handled but an error occured")] + Operation(#[from] B), + #[error("an internal error occured")] + Internal(String), + #[error("the operation was unhandled or unrecognized")] + Unhandled, +} diff --git a/giterated-models/src/handshake.rs b/giterated-models/src/handshake.rs new file mode 100644 index 0000000..639d9c6 --- /dev/null +++ b/giterated-models/src/handshake.rs @@ -0,0 +1,22 @@ +use semver::Version; +use serde::{Deserialize, Serialize}; + +use crate::model::instance::Instance; + +/// Sent by the initiator of a new inter-daemon connection. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct InitiateHandshake { + pub version: Version, +} + +/// Sent in response to [`InitiateHandshake`] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct HandshakeResponse { + pub identity: Instance, + pub version: Version, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct HandshakeFinalize { + pub success: bool, +} diff --git a/giterated-models/src/lib.rs b/giterated-models/src/lib.rs index 69c9adf..87f4104 100644 --- a/giterated-models/src/lib.rs +++ b/giterated-models/src/lib.rs @@ -1,2 +1,5 @@ -pub mod messages; +pub mod error; +pub mod handshake; pub mod model; +pub mod operation; +pub mod values; diff --git a/giterated-models/src/messages/authentication.rs b/giterated-models/src/messages/authentication.rs index 62305f4..3c42126 100644 --- a/giterated-models/src/messages/authentication.rs +++ b/giterated-models/src/messages/authentication.rs @@ -1,3 +1,4 @@ +use secrecy::{Secret, SerializableSecret, Zeroize, CloneableSecret, DebugSecret}; use serde::{Deserialize, Serialize}; use crate::model::{authenticated::UserAuthenticationToken, instance::Instance}; @@ -13,7 +14,7 @@ use super::MessageTarget; pub struct RegisterAccountRequest { pub username: String, pub email: Option, - pub password: String, + pub password: Secret, } impl MessageTarget for RegisterAccountRequest { @@ -42,9 +43,22 @@ pub struct RegisterAccountResponse { pub struct AuthenticationTokenRequest { pub instance: Instance, pub username: String, - pub password: String, + pub password: Secret, } +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Password(pub String); + +impl Zeroize for Password { + fn zeroize(&mut self) { + self.0.zeroize() + } +} + +impl SerializableSecret for Password {} +impl CloneableSecret for Password {} +impl DebugSecret for Password {} + impl MessageTarget for AuthenticationTokenRequest { fn target(&self) -> Option { Some(self.instance.clone()) diff --git a/giterated-models/src/messages/handshake.rs b/giterated-models/src/messages/handshake.rs deleted file mode 100644 index 639d9c6..0000000 --- a/giterated-models/src/messages/handshake.rs +++ /dev/null @@ -1,22 +0,0 @@ -use semver::Version; -use serde::{Deserialize, Serialize}; - -use crate::model::instance::Instance; - -/// Sent by the initiator of a new inter-daemon connection. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct InitiateHandshake { - pub version: Version, -} - -/// Sent in response to [`InitiateHandshake`] -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct HandshakeResponse { - pub identity: Instance, - pub version: Version, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct HandshakeFinalize { - pub success: bool, -} diff --git a/giterated-models/src/messages/repository.rs b/giterated-models/src/messages/repository.rs index 8a9f64f..a797fb8 100644 --- a/giterated-models/src/messages/repository.rs +++ b/giterated-models/src/messages/repository.rs @@ -56,13 +56,12 @@ pub struct RepositoryCreateResponse; /// - Potential User permissions checks #[derive(Clone, Debug, Serialize, Deserialize)] pub struct RepositoryFileInspectRequest { - pub repository: Repository, pub path: RepositoryTreeEntry, } impl MessageTarget for RepositoryFileInspectRequest { fn target(&self) -> Option { - Some(self.repository.instance.clone()) + None } } @@ -118,11 +117,6 @@ pub struct RepositoryIssueLabelsResponse { pub labels: Vec, } -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct IssueLabel { - pub name: String, - pub color: String, -} /// A request to get a repository's issue labels. /// @@ -143,23 +137,6 @@ pub struct RepositoryIssuesResponse { pub issues: Vec, } -/// A request to get a repository's issues. -/// -/// # Authentication -/// - Instance Authentication -/// - Validate request against the `issued_for` public key -/// - Validate User token against the user's instance's public key -/// # Authorization -/// - User Authorization -/// - Potential User permissions checks -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct RepositoryIssue { - pub author: User, - pub id: u64, - pub title: String, - pub contents: String, - pub labels: Vec, -} #[derive(Clone, Debug, Serialize, Deserialize)] pub struct RepositoryInfoRequest { diff --git a/giterated-models/src/model/authenticated.rs b/giterated-models/src/model/authenticated.rs index f44fe78..9989493 100644 --- a/giterated-models/src/model/authenticated.rs +++ b/giterated-models/src/model/authenticated.rs @@ -1,4 +1,4 @@ -use std::{any::type_name, fmt::Debug}; +use std::fmt::Debug; use rsa::{ pkcs1::DecodeRsaPrivateKey, @@ -10,9 +10,9 @@ use rsa::{ use serde::{Deserialize, Serialize}; use tracing::info; -use crate::messages::MessageTarget; +use crate::operation::{GiteratedMessage, GiteratedObject, GiteratedOperation}; -use super::{instance::Instance, user::User}; +use super::{instance::Instance, user::User, MessageTarget}; #[derive(Debug, Serialize, Deserialize)] pub struct UserTokenMetadata { @@ -22,37 +22,33 @@ pub struct UserTokenMetadata { } #[derive(Debug)] -pub struct Authenticated { - pub target_instance: Option, +pub struct Authenticated> { pub source: Vec>, - pub message_type: String, - pub message: T, + pub message: GiteratedMessage, } #[derive(Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] pub struct AuthenticatedPayload { - pub target_instance: Option, pub source: Vec, - pub message_type: String, pub payload: Vec, } -impl From> for AuthenticatedPayload { - fn from(mut value: Authenticated) -> Self { - let payload = bincode::serialize(&value.message).unwrap(); +// impl From> for AuthenticatedPayload { +// fn from(mut value: Authenticated) -> Self { +// let payload = bincode::serialize(&value.message).unwrap(); - AuthenticatedPayload { - target_instance: value.target_instance, - source: value - .source - .drain(..) - .map(|provider| provider.as_ref().authenticate(&payload)) - .collect::>(), - message_type: value.message_type, - payload, - } - } -} +// AuthenticatedPayload { +// target_instance: value.target_instance, +// source: value +// .source +// .drain(..) +// .map(|provider| provider.as_ref().authenticate(&payload)) +// .collect::>(), +// message_type: value.message_type, +// payload, +// } +// } +// } pub trait AuthenticationSourceProvider: Debug { fn authenticate(&self, payload: &Vec) -> AuthenticationSource; @@ -83,30 +79,10 @@ where } } -impl Authenticated { - pub fn new(message: T) -> Self { - Self { - source: vec![], - message_type: type_name::().to_string(), - target_instance: message.target(), - message, - } - } - - pub fn new_for(instance: impl ToOwned, message: T) -> Self { +impl> Authenticated { + pub fn new(message: GiteratedMessage) -> Self { Self { source: vec![], - message_type: type_name::().to_string(), - message, - target_instance: Some(instance.to_owned()), - } - } - - pub fn new_empty(message: T) -> Self { - Self { - source: vec![], - message_type: type_name::().to_string(), - target_instance: message.target(), message, } } @@ -126,8 +102,17 @@ impl Authenticated { .push(Box::new(authentication) as Box); } - pub fn into_payload(self) -> AuthenticatedPayload { - self.into() + pub fn into_payload(mut self) -> AuthenticatedPayload { + let payload = bincode::serialize(&self.message).unwrap(); + + AuthenticatedPayload { + source: self + .source + .drain(..) + .map(|provider| provider.as_ref().authenticate(&payload)) + .collect::>(), + payload, + } } } @@ -160,7 +145,7 @@ impl AuthenticationSourceProvider for InstanceAuthenticator { let private_key = RsaPrivateKey::from_pkcs1_pem(&self.private_key).unwrap(); let signing_key = SigningKey::::new(private_key); - let signature = signing_key.sign_with_rng(&mut rng, &payload); + let signature = signing_key.sign_with_rng(&mut rng, payload); AuthenticationSource::Instance { instance: self.instance.clone(), diff --git a/giterated-models/src/model/instance.rs b/giterated-models/src/model/instance.rs index 45e0b20..f13f34d 100644 --- a/giterated-models/src/model/instance.rs +++ b/giterated-models/src/model/instance.rs @@ -3,6 +3,8 @@ use std::str::FromStr; use serde::{Deserialize, Serialize}; use thiserror::Error; +use crate::operation::GiteratedObject; + pub struct InstanceMeta { pub url: String, pub public_key: String, @@ -30,6 +32,16 @@ pub struct Instance { pub url: String, } +impl GiteratedObject for Instance { + fn object_name(&self) -> &str { + "instance" + } + + fn from_object_str(object_str: &str) -> Result { + Ok(Instance::from_str(object_str).unwrap()) + } +} + impl ToString for Instance { fn to_string(&self) -> String { self.url.clone() diff --git a/giterated-models/src/model/mod.rs b/giterated-models/src/model/mod.rs index e09885d..08397bc 100644 --- a/giterated-models/src/model/mod.rs +++ b/giterated-models/src/model/mod.rs @@ -3,9 +3,17 @@ //! All network data model types that are not directly associated with //! individual requests or responses. +use self::instance::Instance; + pub mod authenticated; pub mod discovery; pub mod instance; pub mod repository; pub mod settings; pub mod user; + +pub trait MessageTarget { + fn target(&self) -> Option { + todo!() + } +} diff --git a/giterated-models/src/model/repository.rs b/giterated-models/src/model/repository.rs index b030580..a6087d8 100644 --- a/giterated-models/src/model/repository.rs +++ b/giterated-models/src/model/repository.rs @@ -3,6 +3,8 @@ use std::str::FromStr; use serde::{Deserialize, Serialize}; +use crate::operation::GiteratedObject; + use super::{instance::Instance, user::User}; /// A repository, defined by the instance it exists on along with @@ -36,6 +38,16 @@ pub struct Repository { pub instance: Instance, } +impl GiteratedObject for Repository { + fn object_name(&self) -> &str { + "repository" + } + + fn from_object_str(object_str: &str) -> Result { + Ok(Repository::from_str(object_str).unwrap()) + } +} + impl ToString for Repository { fn to_string(&self) -> String { format!("{}/{}@{}", self.owner, self.name, self.instance.to_string()) @@ -216,3 +228,18 @@ pub struct RepositorySummary { pub description: Option, pub last_commit: Option, } + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct IssueLabel { + pub name: String, + pub color: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RepositoryIssue { + pub author: User, + pub id: u64, + pub title: String, + pub contents: String, + pub labels: Vec, +} diff --git a/giterated-models/src/model/user.rs b/giterated-models/src/model/user.rs index 5fa8ead..0d95d4a 100644 --- a/giterated-models/src/model/user.rs +++ b/giterated-models/src/model/user.rs @@ -1,8 +1,11 @@ use std::fmt::{Display, Formatter}; use std::str::FromStr; +use secrecy::{CloneableSecret, DebugSecret, SerializableSecret, Zeroize}; use serde::{Deserialize, Serialize}; +use crate::operation::GiteratedObject; + use super::instance::Instance; /// A user, defined by its username and instance. @@ -31,6 +34,16 @@ pub struct User { pub instance: Instance, } +impl GiteratedObject for User { + fn object_name(&self) -> &str { + "user" + } + + fn from_object_str(object_str: &str) -> Result { + Ok(User::from_str(object_str).unwrap()) + } +} + impl Display for User { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}:{}", self.username, self.instance.url) @@ -54,3 +67,16 @@ impl FromStr for User { Ok(Self { username, instance }) } } + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Password(pub String); + +impl Zeroize for Password { + fn zeroize(&mut self) { + self.0.zeroize() + } +} + +impl SerializableSecret for Password {} +impl CloneableSecret for Password {} +impl DebugSecret for Password {} diff --git a/giterated-models/src/operation/instance.rs b/giterated-models/src/operation/instance.rs new file mode 100644 index 0000000..54b1f29 --- /dev/null +++ b/giterated-models/src/operation/instance.rs @@ -0,0 +1,174 @@ +use secrecy::Secret; +use serde::{Deserialize, Serialize}; + +use crate::{ + error::InstanceError, + model::{ + authenticated::UserAuthenticationToken, + instance::Instance, + repository::{Repository, RepositoryVisibility}, + user::{Password, User}, + }, +}; + +use super::{GiteratedOperation, Object, ObjectBackend}; + +/// An account registration request. +/// +/// # Authentication +/// - Instance Authentication +/// - **ONLY ACCEPTED WHEN SAME-INSTANCE** +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RegisterAccountRequest { + pub username: String, + pub email: Option, + pub password: Secret, +} + +impl GiteratedOperation for RegisterAccountRequest { + type Success = UserAuthenticationToken; + type Failure = InstanceError; +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RegisterAccountResponse { + pub token: String, +} + +/// An authentication token request. +/// +/// AKA Login Request +/// +/// # Authentication +/// - Instance Authentication +/// - Identifies the Instance to issue the token for +/// # Authorization +/// - Credentials ([`crate::backend::AuthBackend`]-based) +/// - Identifies the User account to issue a token for +/// - Decrypts user private key to issue to +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AuthenticationTokenRequest { + pub instance: Instance, + pub username: String, + pub password: Secret, +} + +impl GiteratedOperation for AuthenticationTokenRequest { + type Success = UserAuthenticationToken; + type Failure = InstanceError; +} + +/// An authentication token extension request. +/// +/// # Authentication +/// - Instance Authentication +/// - Identifies the Instance to issue the token for +/// - User Authentication +/// - Authenticates the validity of the token +/// # Authorization +/// - Token-based +/// - Validates authorization using token's authenticity +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TokenExtensionRequest { + pub token: UserAuthenticationToken, +} + +impl GiteratedOperation for TokenExtensionRequest { + type Success = Option; + type Failure = InstanceError; +} + +/// A request to create a repository. +/// +/// # Authentication +/// - Instance Authentication +/// - Used to validate User token `issued_for` +/// - User Authentication +/// - Used to source owning user +/// - Used to authorize user token against user's instance +/// # Authorization +/// - Instance Authorization +/// - Used to authorize action using User token requiring a correct `issued_for` and valid issuance from user's instance +/// - User Authorization +/// - Potential User permissions checks +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RepositoryCreateRequest { + pub instance: Option, + pub name: String, + pub description: Option, + pub visibility: RepositoryVisibility, + pub default_branch: String, + pub owner: User, +} + +impl GiteratedOperation for RepositoryCreateRequest { + type Success = Repository; + type Failure = InstanceError; +} + +impl Object<'_, Instance, B> { + pub async fn register_account( + &mut self, + email: Option<&str>, + username: &str, + password: &Secret, + ) -> Result { + self.request::(RegisterAccountRequest { + username: username.to_string(), + email: email.map(|s| s.to_string()), + password: password.clone(), + }) + } + + pub async fn authentication_token( + &mut self, + username: &str, + password: &Secret, + ) -> Result { + self.request::(AuthenticationTokenRequest { + instance: self.inner.clone(), + username: username.to_string(), + password: password.clone(), + }) + } + + pub async fn authentication_token_for( + &mut self, + instance: &Instance, + username: &str, + password: &Secret, + ) -> Result { + self.request::(AuthenticationTokenRequest { + instance: instance.clone(), + username: username.to_string(), + password: password.clone(), + }) + } + + pub async fn token_extension( + &mut self, + token: &UserAuthenticationToken, + ) -> Result, InstanceError> { + self.request::(TokenExtensionRequest { + token: token.clone(), + }) + } + + pub async fn create_repository( + &mut self, + instance: &Instance, + name: &str, + visibility: RepositoryVisibility, + default_branch: &str, + owner: &User, + ) -> Result { + self.request::(RepositoryCreateRequest { + instance: Some(instance.clone()), + name: name.to_string(), + description: None, + visibility, + default_branch: default_branch.to_string(), + owner: owner.clone(), + }) + } +} diff --git a/giterated-models/src/operation/mod.rs b/giterated-models/src/operation/mod.rs new file mode 100644 index 0000000..47c59ad --- /dev/null +++ b/giterated-models/src/operation/mod.rs @@ -0,0 +1,179 @@ +use std::{any::type_name, fmt::Debug, marker::PhantomData}; + +use anyhow::Error; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +use crate::{ + error::{GetValueError, OperationError}, + model::{instance::Instance, MessageTarget}, +}; + +pub mod instance; +pub mod repository; +pub mod user; + +pub trait GiteratedObject: Send + Serialize + DeserializeOwned { + fn object_name(&self) -> &str; + + fn from_object_str(object_str: &str) -> Result; +} + +pub trait GiteratedOperation: Send + Serialize + DeserializeOwned { + type Success: Serialize + DeserializeOwned + Send; + type Failure: Serialize + DeserializeOwned + Send; + + fn operation_name(&self) -> &'static str { + type_name::() + } +} + +pub trait GiteratedObjectValue: Serialize + DeserializeOwned { + type Object: GiteratedObject; + + fn value_name() -> &'static str; +} + +#[derive(Debug, Clone)] +pub struct Object<'b, O: GiteratedObject, B: ObjectBackend + 'b + Send + Sync + Clone> { + inner: O, + backend: B, + _marker: PhantomData<&'b ()>, +} + +#[async_trait::async_trait] +pub trait ObjectBackend: Send + Sync + Sized + Clone { + async fn object_operation + Debug>( + &self, + object: O, + operation: D, + ) -> Result>; + + async fn get_object( + &self, + object_str: &str, + ) -> Result, OperationError>; +} + +impl<'b, B: ObjectBackend + Send + Sync + Clone, O: GiteratedObject> Object<'b, O, B> { + pub unsafe fn new_unchecked(object: O, backend: B) -> Object<'b, O, B> { + Object { + inner: object, + backend, + _marker: PhantomData, + } + } +} + +// impl<'b, O: GiteratedObject, B: ObjectBackend> Object<'b, O, B> { +// pub unsafe fn new_unchecked(value: O, backend: Arc) -> Object<'b, O, B> { +// todo!() +// } +// } + +impl<'b, O: GiteratedObject + Clone + Debug, B: ObjectBackend + Debug + Send + Sync + Clone> + Object<'b, O, B> +{ + pub async fn get + Send>( + &self, + ) -> Result> { + let operation: GetValue = GetValue { + value_name: V::value_name().to_string(), + _marker: PhantomData, + }; + + let _message: GiteratedMessage = GiteratedMessage { + object: self.inner.clone(), + operation: operation.operation_name().to_string(), + payload: operation, + }; + + todo!() + } + + pub fn request + Debug>( + &mut self, + request: R, + ) -> Result { + self.backend.object_operation(self.inner.clone(), request); + + todo!() + } +} + +#[derive(Serialize, Deserialize)] +pub struct GetValue { + value_name: String, + _marker: PhantomData, +} + +impl + Send> GiteratedOperation + for GetValue +{ + fn operation_name(&self) -> &'static str { + "get_value" + } + type Success = V; + type Failure = GetValueError; +} + +#[derive(Serialize)] +#[serde(bound(deserialize = "O: GiteratedObject, V: GiteratedOperation"))] +pub struct GiteratedMessage> { + pub object: O, + pub operation: String, + pub payload: V, +} + +impl> MessageTarget for GiteratedMessage {} + +impl + Debug, O: GiteratedObject + Debug> Debug + for GiteratedMessage +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("GiteratedMessage") + .field("object", &self.object) + .field("operation", &self.operation) + .field("payload", &self.payload) + .finish() + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ObjectRequest(pub String); + +#[derive(Serialize, Deserialize)] +pub struct ObjectResponse(pub Vec); + +impl GiteratedOperation for ObjectRequest { + type Success = ObjectResponse; + + type Failure = ObjectRequestError; +} + +#[derive(Debug, thiserror::Error, Serialize, Deserialize)] +pub enum ObjectRequestError { + #[error("error decoding the object")] + Deserialization(String), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AnyObject(Vec); + +impl GiteratedObject for AnyObject { + fn object_name(&self) -> &str { + "any" + } + + fn from_object_str(object_str: &str) -> Result { + Ok(Self(Vec::from(object_str.as_bytes()))) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AnyOperation(Vec); + +impl GiteratedOperation for AnyOperation { + type Success = Vec; + + type Failure = Vec; +} diff --git a/giterated-models/src/operation/repository.rs b/giterated-models/src/operation/repository.rs new file mode 100644 index 0000000..d38c8a8 --- /dev/null +++ b/giterated-models/src/operation/repository.rs @@ -0,0 +1,101 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ + error::RepositoryError, + model::repository::{IssueLabel, Repository, RepositoryIssue, RepositoryTreeEntry}, +}; + +use super::{GiteratedOperation, Object, ObjectBackend}; + +/// A request to get a repository's information. +/// +/// # Authentication +/// - Instance Authentication +/// - Validate request against the `issued_for` public key +/// - Validate User token against the user's instance's public key +/// # Authorization +/// - User Authorization +/// - Potential User permissions checks +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RepositoryIssuesCountRequest; + +impl GiteratedOperation for RepositoryIssuesCountRequest { + type Success = u64; + type Failure = RepositoryError; +} + +/// A request to get a repository's issues count. +/// +/// # Authentication +/// - Instance Authentication +/// - Validate request against the `issued_for` public key +/// - Validate User token against the user's instance's public key +/// # Authorization +/// - User Authorization +/// - Potential User permissions checks +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RepositoryIssueLabelsRequest; + +impl GiteratedOperation for RepositoryIssueLabelsRequest { + type Success = Vec; + type Failure = RepositoryError; +} + +/// A request to get a repository's issue labels. +/// +/// # Authentication +/// - Instance Authentication +/// - Validate request against the `issued_for` public key +/// - Validate User token against the user's instance's public key +/// # Authorization +/// - User Authorization +/// - Potential User permissions checks +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RepositoryIssuesRequest; + +impl GiteratedOperation for RepositoryIssuesRequest { + type Success = Vec; + type Failure = RepositoryError; +} + +/// A request to inspect the tree of a repository. +/// +/// # Authentication +/// - Instance Authentication +/// - Validate request against the `issued_for` public key +/// - Validate User token against the user's instance's public key +/// # Authorization +/// - User Authorization +/// - Potential User permissions checks +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RepositoryFileInspectRequest { + pub path: RepositoryTreeEntry, +} + +impl GiteratedOperation for RepositoryFileInspectRequest { + type Success = Vec; + type Failure = RepositoryError; +} + +impl Object<'_, Repository, B> { + pub async fn issues_count(&mut self) -> Result { + self.request::(RepositoryIssuesCountRequest) + } + + pub async fn issue_labels(&mut self) -> Result, RepositoryError> { + self.request::(RepositoryIssueLabelsRequest) + } + + pub async fn issues(&mut self) -> Result, RepositoryError> { + self.request::(RepositoryIssuesRequest) + } + + pub async fn inspect_files( + &mut self, + entry: &RepositoryTreeEntry, + ) -> Result, RepositoryError> { + self.request::(RepositoryFileInspectRequest { + path: entry.clone(), + }) + } +} diff --git a/giterated-models/src/operation/user.rs b/giterated-models/src/operation/user.rs new file mode 100644 index 0000000..48c71d6 --- /dev/null +++ b/giterated-models/src/operation/user.rs @@ -0,0 +1,31 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ + error::UserError, + model::{instance::Instance, repository::Repository, user::User}, +}; + +use super::{GiteratedOperation, Object, ObjectBackend}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct UserRepositoriesRequest { + pub instance: Instance, + pub user: User, +} + +impl GiteratedOperation for UserRepositoriesRequest { + type Success = Vec; + type Failure = UserError; +} + +impl Object<'_, User, B> { + pub async fn repositories( + &mut self, + instance: &Instance, + ) -> Result, UserError> { + self.request::(UserRepositoriesRequest { + instance: instance.clone(), + user: self.inner.clone(), + }) + } +} diff --git a/giterated-models/src/values/instance.rs b/giterated-models/src/values/instance.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/giterated-models/src/values/instance.rs @@ -0,0 +1 @@ + diff --git a/giterated-models/src/values/mod.rs b/giterated-models/src/values/mod.rs new file mode 100644 index 0000000..e741f77 --- /dev/null +++ b/giterated-models/src/values/mod.rs @@ -0,0 +1,3 @@ +pub mod instance; +pub mod repository; +pub mod user; diff --git a/giterated-models/src/values/repository.rs b/giterated-models/src/values/repository.rs new file mode 100644 index 0000000..fedf67a --- /dev/null +++ b/giterated-models/src/values/repository.rs @@ -0,0 +1,40 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ + model::repository::{Repository, RepositoryVisibility}, + operation::GiteratedObjectValue, +}; + +// pub struct RepositorySetting(pub V); + +// impl + Send> GiteratedOperation +// for RepositorySetting +// { +// fn operation_name(&self) -> &'static str { +// "setting_get" +// } +// type Success = V; +// type Failure = GetValueError; +// } + +#[derive(Debug, Hash, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Description(pub String); + +impl GiteratedObjectValue for Description { + type Object = Repository; + + fn value_name() -> &'static str { + "description" + } +} + +#[derive(Debug, Hash, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Visibility(pub RepositoryVisibility); + +impl GiteratedObjectValue for Visibility { + type Object = Repository; + + fn value_name() -> &'static str { + "visibility" + } +} diff --git a/giterated-models/src/values/user.rs b/giterated-models/src/values/user.rs new file mode 100644 index 0000000..1b142c0 --- /dev/null +++ b/giterated-models/src/values/user.rs @@ -0,0 +1,37 @@ +use serde::{Deserialize, Serialize}; + +use crate::{model::user::User, operation::GiteratedObjectValue}; + +// pub struct UserSetting(pub V); + +// impl + Send> GiteratedOperation +// for UserSetting +// { +// fn operation_name(&self) -> &'static str { +// "setting_get" +// } +// type Success = V; +// type Failure = GetValueError; +// } + +#[derive(Debug, Hash, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Bio(pub String); + +impl GiteratedObjectValue for Bio { + type Object = User; + + fn value_name() -> &'static str { + "bio" + } +} + +#[derive(Debug, Hash, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct DisplayName(pub String); + +impl GiteratedObjectValue for DisplayName { + type Object = User; + + fn value_name() -> &'static str { + "display_name" + } +}