//! # API tokens management use crate::app_config::AppConfig; use crate::constants; use crate::utils::time_utils::time; use actix_http::Method; use basic_jwt::{JWTPrivateKey, JWTPublicKey}; use std::path::Path; use std::str::FromStr; #[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)] pub struct TokenID(pub uuid::Uuid); impl TokenID { /// Parse a string as a token id pub fn parse(t: &str) -> anyhow::Result { Ok(Self(uuid::Uuid::parse_str(t)?)) } } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq)] pub struct TokenRight { verb: TokenVerb, path: String, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] pub struct TokenRights(Vec); impl TokenRights { pub fn check_error(&self) -> Option<&'static str> { for r in &self.0 { if !r.path.starts_with("/api/") { return Some("All API rights shall start with /api/"); } if r.path.len() > constants::API_TOKEN_RIGHT_PATH_MAX_LENGTH { return Some("An API path shall not exceed maximum URL size!"); } } None } pub fn contains(&self, verb: TokenVerb, path: &str) -> bool { let req_path_split = path.split('/').collect::>(); 'root: for r in &self.0 { if r.verb != verb { continue 'root; } let mut last_idx = 0; for (idx, part) in r.path.split('/').enumerate() { if idx >= req_path_split.len() { continue 'root; } if part != "*" && part != req_path_split[idx] { continue 'root; } last_idx = idx; } // Check we visited the whole path if last_idx + 1 == req_path_split.len() { return true; } } false } } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] pub struct Token { pub id: TokenID, pub name: String, pub description: String, created: u64, updated: u64, #[serde(skip_serializing_if = "Option::is_none")] pub pub_key: Option, pub rights: TokenRights, pub last_used: u64, pub ip_restriction: Option, pub max_inactivity: Option, } impl Token { /// Turn the token into a JSON string fn save(&self) -> anyhow::Result<()> { let json = serde_json::to_string(self)?; std::fs::write(AppConfig::get().api_token_definition_path(self.id), json)?; Ok(()) } /// Load token information from a file fn load_from_file(path: &Path) -> anyhow::Result { Ok(serde_json::from_str(&std::fs::read_to_string(path)?)?) } /// Check whether a token is expired or not pub fn is_expired(&self) -> bool { if let Some(max_inactivity) = self.max_inactivity { if max_inactivity + self.last_used < time() { return true; } } false } /// Check whether last_used shall be updated or not pub fn should_update_last_activity(&self) -> bool { self.last_used + 3600 < time() } } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, Eq, PartialEq)] pub enum TokenVerb { GET, POST, PUT, PATCH, DELETE, } impl TokenVerb { pub fn as_method(&self) -> Method { match self { TokenVerb::GET => Method::GET, TokenVerb::POST => Method::POST, TokenVerb::PUT => Method::PUT, TokenVerb::PATCH => Method::PATCH, TokenVerb::DELETE => Method::DELETE, } } } impl FromStr for TokenVerb { type Err = (); fn from_str(s: &str) -> Result { match s { "GET" => Ok(TokenVerb::GET), "POST" => Ok(TokenVerb::POST), "PUT" => Ok(TokenVerb::PUT), "PATCH" => Ok(TokenVerb::PATCH), "DELETE" => Ok(TokenVerb::DELETE), _ => Err(()), } } } /// Structure used to create a token #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] pub struct NewToken { pub name: String, pub description: String, pub rights: TokenRights, pub ip_restriction: Option, pub max_inactivity: Option, } impl NewToken { /// Check for error in token pub fn check_error(&self) -> Option<&'static str> { if self.name.len() < constants::API_TOKEN_NAME_MIN_LENGTH { return Some("Name is too short!"); } if self.name.len() > constants::API_TOKEN_NAME_MAX_LENGTH { return Some("Name is too long!"); } if self.description.len() < constants::API_TOKEN_DESCRIPTION_MIN_LENGTH { return Some("Description is too short!"); } if self.description.len() > constants::API_TOKEN_DESCRIPTION_MAX_LENGTH { return Some("Description is too long!"); } if let Some(err) = self.rights.check_error() { return Some(err); } if let Some(t) = self.max_inactivity { if t < 3600 { return Some("API tokens shall be valid for at least 1 hour!"); } } None } } /// Create a new Token pub async fn create(t: &NewToken) -> anyhow::Result<(Token, JWTPrivateKey)> { let priv_key = JWTPrivateKey::generate_ec384_signing_key()?; let pub_key = priv_key.to_public_key()?; let token = Token { name: t.name.to_string(), description: t.description.to_string(), id: TokenID(uuid::Uuid::new_v4()), created: time(), updated: time(), pub_key: Some(pub_key), rights: t.rights.clone(), last_used: time(), ip_restriction: t.ip_restriction, max_inactivity: t.max_inactivity, }; token.save()?; Ok((token, priv_key)) } /// Get the entire list of api tokens pub async fn full_list() -> anyhow::Result> { let mut list = Vec::new(); for f in std::fs::read_dir(AppConfig::get().api_tokens_path())? { list.push(Token::load_from_file(&f?.path())?); } Ok(list) } /// Get the information about a single token pub async fn get_single(id: TokenID) -> anyhow::Result { Token::load_from_file(&AppConfig::get().api_token_definition_path(id)) } /// Update API tokens rights pub async fn update_rights(id: TokenID, rights: TokenRights) -> anyhow::Result<()> { let mut token = get_single(id).await?; token.rights = rights; token.updated = time(); token.save()?; Ok(()) } /// Set last_used value of token pub async fn refresh_last_used(id: TokenID) -> anyhow::Result<()> { let mut token = get_single(id).await?; token.last_used = time(); token.save()?; Ok(()) } /// Delete an API token pub async fn delete(id: TokenID) -> anyhow::Result<()> { let path = AppConfig::get().api_token_definition_path(id); if path.exists() { std::fs::remove_file(path)?; } Ok(()) } #[cfg(test)] mod test { use crate::api_tokens::{TokenRight, TokenRights, TokenVerb}; #[test] fn test_rights_patch() { let rights = TokenRights(vec![ TokenRight { path: "/api/vm/*".to_string(), verb: TokenVerb::GET, }, TokenRight { path: "/api/vm/a".to_string(), verb: TokenVerb::PUT, }, TokenRight { path: "/api/vm/a/other".to_string(), verb: TokenVerb::DELETE, }, TokenRight { path: "/api/net/create".to_string(), verb: TokenVerb::POST, }, ]); assert!(rights.contains(TokenVerb::GET, "/api/vm/ab")); assert!(!rights.contains(TokenVerb::GET, "/api/vm")); assert!(!rights.contains(TokenVerb::GET, "/api/vm/ab/c")); assert!(rights.contains(TokenVerb::PUT, "/api/vm/a")); assert!(!rights.contains(TokenVerb::PUT, "/api/vm/other")); assert!(rights.contains(TokenVerb::POST, "/api/net/create")); assert!(!rights.contains(TokenVerb::GET, "/api/net/create")); assert!(!rights.contains(TokenVerb::POST, "/api/net/b")); assert!(!rights.contains(TokenVerb::POST, "/api/net/create/b")); } }