All checks were successful
		
		
	
	continuous-integration/drone/push Build is passing
				
			
		
			
				
	
	
		
			242 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			242 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| use s3::error::S3Error;
 | |
| use s3::request::ResponseData;
 | |
| use s3::{Bucket, BucketConfiguration};
 | |
| use std::str::FromStr;
 | |
| use thiserror::Error;
 | |
| 
 | |
| use crate::app_config::AppConfig;
 | |
| use crate::constants::TOKEN_LEN;
 | |
| use crate::utils::base_utils::{curr_time, format_time, rand_str};
 | |
| 
 | |
| type HttpClient = ruma::client::http_client::HyperNativeTls;
 | |
| pub type RumaClient = ruma::Client<HttpClient>;
 | |
| 
 | |
| #[derive(Error, Debug)]
 | |
| pub enum UserError {
 | |
|     #[error("failed to fetch user configuration: {0}")]
 | |
|     FetchUserConfig(S3Error),
 | |
|     #[error("missing matrix token")]
 | |
|     MissingMatrixToken,
 | |
| }
 | |
| 
 | |
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
 | |
| pub struct UserID(pub String);
 | |
| 
 | |
| impl UserID {
 | |
|     fn conf_path_in_bucket(&self) -> String {
 | |
|         format!("confs/{}.json", urlencoding::encode(&self.0))
 | |
|     }
 | |
| }
 | |
| 
 | |
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
 | |
| pub struct User {
 | |
|     pub id: UserID,
 | |
|     pub name: String,
 | |
|     pub email: String,
 | |
| }
 | |
| 
 | |
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq)]
 | |
| pub struct APIClientID(pub uuid::Uuid);
 | |
| 
 | |
| impl APIClientID {
 | |
|     pub fn generate() -> Self {
 | |
|         Self(uuid::Uuid::new_v4())
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl FromStr for APIClientID {
 | |
|     type Err = uuid::Error;
 | |
| 
 | |
|     fn from_str(s: &str) -> Result<Self, Self::Err> {
 | |
|         Ok(Self(uuid::Uuid::from_str(s)?))
 | |
|     }
 | |
| }
 | |
| 
 | |
| /// Single API client information
 | |
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
 | |
| pub struct APIClient {
 | |
|     /// Client unique ID
 | |
|     pub id: APIClientID,
 | |
| 
 | |
|     /// Client description
 | |
|     pub description: String,
 | |
| 
 | |
|     /// Restricted API network for token
 | |
|     pub network: Option<ipnet::IpNet>,
 | |
| 
 | |
|     /// Client secret
 | |
|     pub secret: String,
 | |
| 
 | |
|     /// Client creation time
 | |
|     pub created: u64,
 | |
| 
 | |
|     /// Client last usage time
 | |
|     pub used: u64,
 | |
| 
 | |
|     /// Read only access
 | |
|     pub readonly_client: bool,
 | |
| }
 | |
| 
 | |
| impl APIClient {
 | |
|     pub fn fmt_created(&self) -> String {
 | |
|         format_time(self.created).unwrap_or_default()
 | |
|     }
 | |
| 
 | |
|     pub fn fmt_used(&self) -> String {
 | |
|         format_time(self.used).unwrap_or_default()
 | |
|     }
 | |
| 
 | |
|     pub fn need_update_last_used(&self) -> bool {
 | |
|         self.used + 60 * 15 < curr_time().unwrap()
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl APIClient {
 | |
|     /// Generate a new API client
 | |
|     pub fn generate(description: String, network: Option<ipnet::IpNet>) -> Self {
 | |
|         Self {
 | |
|             id: APIClientID::generate(),
 | |
|             description,
 | |
|             network,
 | |
|             secret: rand_str(TOKEN_LEN),
 | |
|             created: curr_time().unwrap(),
 | |
|             used: curr_time().unwrap(),
 | |
|             readonly_client: true,
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| #[derive(serde::Serialize, serde::Deserialize, Clone)]
 | |
| pub struct UserConfig {
 | |
|     /// Target user ID
 | |
|     pub user_id: UserID,
 | |
| 
 | |
|     /// Configuration creation time
 | |
|     pub created: u64,
 | |
| 
 | |
|     /// Configuration last update time
 | |
|     pub updated: u64,
 | |
| 
 | |
|     /// Current user matrix token
 | |
|     pub matrix_token: String,
 | |
| 
 | |
|     /// API clients
 | |
|     pub clients: Vec<APIClient>,
 | |
| }
 | |
| 
 | |
| impl UserConfig {
 | |
|     /// Create S3 bucket if required
 | |
|     pub async fn create_bucket_if_required() -> anyhow::Result<()> {
 | |
|         if AppConfig::get().s3_skip_auto_create_bucket {
 | |
|             log::debug!("Skipping bucket existence check");
 | |
|             return Ok(());
 | |
|         }
 | |
| 
 | |
|         let bucket = AppConfig::get().s3_bucket()?;
 | |
| 
 | |
|         match bucket.location().await {
 | |
|             Ok(_) => {
 | |
|                 log::debug!("The bucket already exists.");
 | |
|                 return Ok(());
 | |
|             }
 | |
|             Err(S3Error::HttpFailWithBody(404, s)) if s.contains("<Code>NoSuchKey</Code>") => {
 | |
|                 log::warn!("Failed to fetch bucket location, but it seems that bucket exists.");
 | |
|                 return Ok(());
 | |
|             }
 | |
|             Err(S3Error::HttpFailWithBody(404, s)) if s.contains("<Code>NoSuchBucket</Code>") => {
 | |
|                 log::warn!("The bucket does not seem to exists, trying to create it!")
 | |
|             }
 | |
|             Err(e) => {
 | |
|                 log::error!("Got unexpected error when querying bucket info: {e}");
 | |
|                 return Err(e.into());
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         Bucket::create_with_path_style(
 | |
|             &bucket.name,
 | |
|             bucket.region,
 | |
|             AppConfig::get().s3_credentials()?,
 | |
|             BucketConfiguration::private(),
 | |
|         )
 | |
|         .await?;
 | |
| 
 | |
|         Ok(())
 | |
|     }
 | |
| 
 | |
|     /// Get current user configuration
 | |
|     pub async fn load(user_id: &UserID, allow_non_existing: bool) -> anyhow::Result<Self> {
 | |
|         let res: Result<ResponseData, S3Error> = AppConfig::get()
 | |
|             .s3_bucket()?
 | |
|             .get_object(user_id.conf_path_in_bucket())
 | |
|             .await;
 | |
| 
 | |
|         match (res, allow_non_existing) {
 | |
|             (Ok(res), _) => Ok(serde_json::from_slice(res.as_slice())?),
 | |
|             (Err(S3Error::HttpFailWithBody(404, _)), true) => {
 | |
|                 log::warn!("User configuration does not exists, generating a new one...");
 | |
|                 Ok(Self {
 | |
|                     user_id: user_id.clone(),
 | |
|                     created: curr_time()?,
 | |
|                     updated: curr_time()?,
 | |
|                     matrix_token: "".to_string(),
 | |
|                     clients: vec![],
 | |
|                 })
 | |
|             }
 | |
|             (Err(e), _) => Err(UserError::FetchUserConfig(e).into()),
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /// Set user configuration
 | |
|     pub async fn save(&mut self) -> anyhow::Result<()> {
 | |
|         log::info!("Saving new configuration for user {:?}", self.user_id);
 | |
| 
 | |
|         self.updated = curr_time()?;
 | |
| 
 | |
|         // Save updated configuration
 | |
|         AppConfig::get()
 | |
|             .s3_bucket()?
 | |
|             .put_object(
 | |
|                 self.user_id.conf_path_in_bucket(),
 | |
|                 &serde_json::to_vec(self)?,
 | |
|             )
 | |
|             .await?;
 | |
| 
 | |
|         Ok(())
 | |
|     }
 | |
| 
 | |
|     /// Get current user matrix token, in an obfuscated form
 | |
|     pub fn obfuscated_matrix_token(&self) -> String {
 | |
|         self.matrix_token
 | |
|             .chars()
 | |
|             .enumerate()
 | |
|             .map(|(num, c)| match num {
 | |
|                 0 | 1 => c,
 | |
|                 _ => 'X',
 | |
|             })
 | |
|             .collect()
 | |
|     }
 | |
| 
 | |
|     /// Find a client by its id
 | |
|     pub fn find_client_by_id(&self, id: &APIClientID) -> Option<&APIClient> {
 | |
|         self.clients.iter().find(|c| &c.id == id)
 | |
|     }
 | |
| 
 | |
|     /// Find a client by its id and get a mutable reference
 | |
|     pub fn find_client_by_id_mut(&mut self, id: &APIClientID) -> Option<&mut APIClient> {
 | |
|         self.clients.iter_mut().find(|c| &c.id == id)
 | |
|     }
 | |
| 
 | |
|     /// Get a matrix client instance for the current user
 | |
|     pub async fn matrix_client(&self) -> anyhow::Result<RumaClient> {
 | |
|         if self.matrix_token.is_empty() {
 | |
|             return Err(UserError::MissingMatrixToken.into());
 | |
|         }
 | |
| 
 | |
|         Ok(ruma::Client::builder()
 | |
|             .homeserver_url(AppConfig::get().matrix_homeserver.to_string())
 | |
|             .access_token(Some(self.matrix_token.clone()))
 | |
|             .build()
 | |
|             .await?)
 | |
|     }
 | |
| }
 |