Pierre HUBERT
c7de64cc02
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"));
|
|
}
|
|
}
|