All checks were successful
		
		
	
	continuous-integration/drone/push Build is passing
				
			Make it possible to create token authorized to query predetermined set of routes. Reviewed-on: #9 Co-authored-by: Pierre HUBERT <pierre.git@communiquons.org> Co-committed-by: Pierre HUBERT <pierre.git@communiquons.org>
		
			
				
	
	
		
			300 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			300 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| //! # 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<Self> {
 | |
|         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<TokenRight>);
 | |
| 
 | |
| 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::<Vec<_>>();
 | |
| 
 | |
|         '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<JWTPublicKey>,
 | |
|     pub rights: TokenRights,
 | |
|     pub last_used: u64,
 | |
|     pub ip_restriction: Option<ipnetwork::IpNetwork>,
 | |
|     pub max_inactivity: Option<u64>,
 | |
| }
 | |
| 
 | |
| 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<Self> {
 | |
|         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<Self, Self::Err> {
 | |
|         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<ipnetwork::IpNetwork>,
 | |
|     pub max_inactivity: Option<u64>,
 | |
| }
 | |
| 
 | |
| 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<Vec<Token>> {
 | |
|     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> {
 | |
|     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"));
 | |
|     }
 | |
| }
 |