From ab7907d947e44eba8341729fa77a75295b303c6e Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 8 Apr 2024 22:19:28 +0200 Subject: [PATCH 01/36] WIP --- virtweb_backend/Cargo.lock | 63 ++++++++++ virtweb_backend/Cargo.toml | 3 + virtweb_backend/src/api_tokens.rs | 105 +++++++++++++++++ virtweb_backend/src/app_config.rs | 4 + virtweb_backend/src/constants.rs | 15 +++ .../src/controllers/api_tokens_controller.rs | 49 ++++++++ virtweb_backend/src/controllers/mod.rs | 1 + .../src/controllers/server_controller.rs | 12 ++ virtweb_backend/src/lib.rs | 1 + virtweb_backend/src/main.rs | 26 ++++- virtweb_backend/src/utils/jwt_utils.rs | 109 ++++++++++++++++++ virtweb_backend/src/utils/mod.rs | 1 + 12 files changed, 387 insertions(+), 2 deletions(-) create mode 100644 virtweb_backend/src/api_tokens.rs create mode 100644 virtweb_backend/src/controllers/api_tokens_controller.rs create mode 100644 virtweb_backend/src/utils/jwt_utils.rs diff --git a/virtweb_backend/Cargo.lock b/virtweb_backend/Cargo.lock index 3c80db5..18c6836 100644 --- a/virtweb_backend/Cargo.lock +++ b/virtweb_backend/Cargo.lock @@ -1224,8 +1224,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -1596,6 +1598,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" +dependencies = [ + "base64 0.21.7", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -2042,6 +2059,16 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64 0.22.0", + "serde", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -2376,6 +2403,21 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rust-embed" version = "8.3.0" @@ -2620,6 +2662,18 @@ dependencies = [ "quote", ] +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "slab" version = "0.4.9" @@ -3020,6 +3074,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.0" @@ -3137,6 +3197,7 @@ dependencies = [ "futures-util", "image", "ipnetwork", + "jsonwebtoken", "lazy-regex", "lazy_static", "light-openid", @@ -3144,9 +3205,11 @@ dependencies = [ "mime_guess", "nix", "num", + "pem", "quick-xml", "rand", "reqwest", + "ring", "rust-embed", "serde", "serde_json", diff --git a/virtweb_backend/Cargo.toml b/virtweb_backend/Cargo.toml index 5fb5ae8..08e7383 100644 --- a/virtweb_backend/Cargo.toml +++ b/virtweb_backend/Cargo.toml @@ -45,3 +45,6 @@ rust-embed = { version = "8.3.0" } mime_guess = "2.0.4" dotenvy = "0.15.7" nix = { version = "0.28.0", features = ["net"] } +jsonwebtoken = "9.3.0" +ring = "0.17.8" +pem = "3.0.4" \ No newline at end of file diff --git a/virtweb_backend/src/api_tokens.rs b/virtweb_backend/src/api_tokens.rs new file mode 100644 index 0000000..8ec405b --- /dev/null +++ b/virtweb_backend/src/api_tokens.rs @@ -0,0 +1,105 @@ +//! # API tokens management + +use crate::constants; +use crate::utils::jwt_utils; +use crate::utils::jwt_utils::{TokenPrivKey, TokenPubKey}; +use crate::utils::time_utils::time; + +#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)] +pub struct TokenID(pub uuid::Uuid); + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct Token { + pub name: String, + pub description: String, + pub id: TokenID, + created: u64, + updated: u64, + pub pub_key: TokenPubKey, + pub rights: Vec, + pub last_used: Option, + pub ip_restriction: Option, + pub delete_after_inactivity: Option, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, Eq, PartialEq)] +pub enum TokenVerb { + GET, + POST, + PUT, + PATCH, + DELETE, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct TokenRights { + verb: TokenVerb, + uri: String, +} + +/// Structure used to create a token +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct NewToken { + pub name: String, + pub description: String, + pub rights: Vec, + pub ip_restriction: Option, + pub delete_after_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!"); + } + + for r in &self.rights { + if !r.uri.starts_with("/api/") { + return Some("All API rights shall start with /api/"); + } + } + + if let Some(t) = self.delete_after_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(token: &NewToken) -> anyhow::Result<(Token, TokenPrivKey)> { + let (pub_key, priv_key) = jwt_utils::generate_key_pair()?; + + let full_token = Token { + name: token.name.to_string(), + description: token.description.to_string(), + id: TokenID(uuid::Uuid::new_v4()), + created: time(), + updated: time(), + pub_key, + rights: token.rights.clone(), + last_used: Some(time()), + ip_restriction: token.ip_restriction, + delete_after_inactivity: token.delete_after_inactivity, + }; + + // TODO : save + + Ok((full_token, priv_key)) +} diff --git a/virtweb_backend/src/app_config.rs b/virtweb_backend/src/app_config.rs index 77f74ab..bfa577e 100644 --- a/virtweb_backend/src/app_config.rs +++ b/virtweb_backend/src/app_config.rs @@ -268,6 +268,10 @@ impl AppConfig { self.definitions_path() .join(format!("nwfilter-{}.json", name.0)) } + + pub fn api_tokens_path(&self) -> PathBuf { + self.storage_path().join(constants::STORAGE_TOKENS_DIR) + } } #[derive(Debug, Clone, serde::Serialize)] diff --git a/virtweb_backend/src/constants.rs b/virtweb_backend/src/constants.rs index 7942fcf..6cfbe9b 100644 --- a/virtweb_backend/src/constants.rs +++ b/virtweb_backend/src/constants.rs @@ -89,3 +89,18 @@ pub const NAT_MODE_ENV_VAR_NAME: &str = "NAT_MODE"; /// Nat hook file path pub const NAT_HOOK_PATH: &str = "/etc/libvirt/hooks/network"; + +/// Directory where API tokens are stored, inside storage directory +pub const STORAGE_TOKENS_DIR: &str = "tokens"; + +/// API token name min length +pub const API_TOKEN_NAME_MIN_LENGTH: usize = 3; + +/// API token name max length +pub const API_TOKEN_NAME_MAX_LENGTH: usize = 30; + +/// API token description min length +pub const API_TOKEN_DESCRIPTION_MIN_LENGTH: usize = 5; + +/// API token description max length +pub const API_TOKEN_DESCRIPTION_MAX_LENGTH: usize = 30; diff --git a/virtweb_backend/src/controllers/api_tokens_controller.rs b/virtweb_backend/src/controllers/api_tokens_controller.rs new file mode 100644 index 0000000..e36a6b7 --- /dev/null +++ b/virtweb_backend/src/controllers/api_tokens_controller.rs @@ -0,0 +1,49 @@ +//! # API tokens management + +use crate::api_tokens; +use crate::api_tokens::NewToken; +use crate::controllers::api_tokens_controller::rest_token::RestToken; +use crate::controllers::HttpResult; +use crate::utils::jwt_utils::TokenPrivKey; +use actix_web::{web, HttpResponse}; + +/// Create a special module for REST token to enforce usage of constructor function +mod rest_token { + use crate::api_tokens::Token; + use crate::utils::jwt_utils::TokenPubKey; + + #[derive(serde::Serialize)] + pub struct RestToken { + token: Token, + } + + impl RestToken { + pub fn new(mut token: Token) -> Self { + token.pub_key = TokenPubKey::None; + Self { token } + } + } +} + +#[derive(serde::Serialize)] +struct CreateTokenResult { + token: RestToken, + priv_key: TokenPrivKey, +} + +/// Create a new API token +pub async fn create(new_token: web::Json) -> HttpResult { + if let Some(err) = new_token.check_error() { + log::error!("Failed to validate new API token information! {err}"); + return Ok(HttpResponse::BadRequest().json(format!( + "Failed to validate new API token information! {err}" + ))); + } + + let (token, priv_key) = api_tokens::create(&new_token).await?; + + Ok(HttpResponse::Ok().json(CreateTokenResult { + token: RestToken::new(token), + priv_key, + })) +} diff --git a/virtweb_backend/src/controllers/mod.rs b/virtweb_backend/src/controllers/mod.rs index 402d2d4..7c41010 100644 --- a/virtweb_backend/src/controllers/mod.rs +++ b/virtweb_backend/src/controllers/mod.rs @@ -6,6 +6,7 @@ use std::error::Error; use std::fmt::{Display, Formatter}; use std::io::ErrorKind; +pub mod api_tokens_controller; pub mod auth_controller; pub mod iso_controller; pub mod network_controller; diff --git a/virtweb_backend/src/controllers/server_controller.rs b/virtweb_backend/src/controllers/server_controller.rs index 0310e24..1e729ed 100644 --- a/virtweb_backend/src/controllers/server_controller.rs +++ b/virtweb_backend/src/controllers/server_controller.rs @@ -51,6 +51,8 @@ struct ServerConstraints { nwfilter_comment_size: LenConstraints, nwfilter_priority: SLenConstraints, nwfilter_selectors_count: LenConstraints, + api_token_name_size: LenConstraints, + api_token_description_size: LenConstraints, } pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder { @@ -98,6 +100,16 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder { max: 1000, }, nwfilter_selectors_count: LenConstraints { min: 0, max: 1 }, + + api_token_name_size: LenConstraints { + min: constants::API_TOKEN_NAME_MIN_LENGTH, + max: constants::API_TOKEN_NAME_MAX_LENGTH, + }, + + api_token_description_size: LenConstraints { + min: constants::API_TOKEN_DESCRIPTION_MIN_LENGTH, + max: constants::API_TOKEN_DESCRIPTION_MAX_LENGTH, + }, }, }) } diff --git a/virtweb_backend/src/lib.rs b/virtweb_backend/src/lib.rs index 356a5e3..078fee9 100644 --- a/virtweb_backend/src/lib.rs +++ b/virtweb_backend/src/lib.rs @@ -1,4 +1,5 @@ pub mod actors; +pub mod api_tokens; pub mod app_config; pub mod constants; pub mod controllers; diff --git a/virtweb_backend/src/main.rs b/virtweb_backend/src/main.rs index d05a110..c2db550 100644 --- a/virtweb_backend/src/main.rs +++ b/virtweb_backend/src/main.rs @@ -22,8 +22,8 @@ use virtweb_backend::constants::{ MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION, SESSION_COOKIE_NAME, }; use virtweb_backend::controllers::{ - auth_controller, iso_controller, network_controller, nwfilter_controller, server_controller, - static_controller, vm_controller, + api_tokens_controller, auth_controller, iso_controller, network_controller, + nwfilter_controller, server_controller, static_controller, vm_controller, }; use virtweb_backend::libvirt_client::LibVirtClient; use virtweb_backend::middlewares::auth_middleware::AuthChecker; @@ -50,6 +50,7 @@ async fn main() -> std::io::Result<()> { files_utils::create_directory_if_missing(AppConfig::get().disks_storage_path()).unwrap(); files_utils::create_directory_if_missing(AppConfig::get().nat_path()).unwrap(); files_utils::create_directory_if_missing(AppConfig::get().definitions_path()).unwrap(); + files_utils::create_directory_if_missing(AppConfig::get().api_tokens_path()).unwrap(); let conn = Data::new(LibVirtClient( LibVirtActor::connect() @@ -276,6 +277,27 @@ async fn main() -> std::io::Result<()> { "/api/nwfilter/{uid}", web::delete().to(nwfilter_controller::delete), ) + // API tokens controller + .route( + "/api/tokens/create", + web::post().to(api_tokens_controller::create), + ) + /* TODO .route( + "/api/tokens/list", + web::get().to(api_tokens_controller::list), + ) + .route( + "/api/tokens/{uid}", + web::get().to(api_tokens_controller::get_single), + ) + .route( + "/api/tokens/{uid}", + web::put().to(api_tokens_controller::update), + ) + .route( + "/api/tokens/{uid}", + web::delete().to(api_tokens_controller::delete), + )*/ // Static assets .route("/", web::get().to(static_controller::root_index)) .route( diff --git a/virtweb_backend/src/utils/jwt_utils.rs b/virtweb_backend/src/utils/jwt_utils.rs new file mode 100644 index 0000000..67819c0 --- /dev/null +++ b/virtweb_backend/src/utils/jwt_utils.rs @@ -0,0 +1,109 @@ +use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Validation}; +use ring::signature::{KeyPair, UnparsedPublicKey}; +use serde::de::DeserializeOwned; +use serde::Serialize; + +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] +#[serde(tag = "alg")] +pub enum TokenPubKey { + /// This variant DOES make crash the program. It MUST NOT be serialized. + /// + /// It is a hack to hide public key when getting the list of tokens + None, + + /// ECDSA with SHA2-384 variant + ES384 { r#pub: String }, +} + +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] +#[serde(tag = "alg")] +pub enum TokenPrivKey { + ES384 { r#priv: String }, +} + +/// Generate a new token keypair +pub fn generate_key_pair() -> anyhow::Result<(TokenPubKey, TokenPrivKey)> { + let doc = ring::signature::EcdsaKeyPair::generate_pkcs8( + &ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING, + &ring::rand::SystemRandom::new(), + )?; + + let priv_pem = pem::encode(&pem::Pem::new("PRIVATE KEY", doc.as_ref())); + + let pair = ring::signature::EcdsaKeyPair::from_pkcs8( + &ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING, + doc.as_ref(), + &ring::rand::SystemRandom::new(), + )?; + let pub_pem = pem::encode(&pem::Pem::new("PUBLIC KEY", pair.public_key().as_ref())); + + + let pk = pair.public_key(); + let unp = UnparsedPublicKey::new(&ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING, pk.as_ref()); + + let decoding_key = DecodingKey::from_ec_pem(pub_pem.as_bytes()).expect("aie ai"); + + Ok(( + TokenPubKey::ES384 { r#pub: pub_pem }, + TokenPrivKey::ES384 { r#priv: priv_pem }, + )) +} + +/// Sign JWT with a private key +pub fn sign_jwt(key: &TokenPrivKey, claims: &C) -> anyhow::Result { + match key { + TokenPrivKey::ES384 { r#priv } => { + let encoding_key = EncodingKey::from_ec_pem(r#priv.as_bytes())?; + + Ok(jsonwebtoken::encode( + &jsonwebtoken::Header::new(Algorithm::ES384), + &claims, + &encoding_key, + )?) + } + } +} + +/// Validate a given JWT +pub fn validate_jwt(key: &TokenPubKey, token: &str) -> anyhow::Result { + match key { + TokenPubKey::ES384 { r#pub } => { + let decoding_key = DecodingKey::from_ec_pem(r#pub.as_bytes())?; + + let validation = Validation::new(Algorithm::ES384); + Ok(jsonwebtoken::decode::(token, &decoding_key, &validation)?.claims) + } + TokenPubKey::None => { + panic!("A public key is required!") + } + } +} + +#[cfg(test)] +mod test { + use crate::utils::jwt_utils::{generate_key_pair, sign_jwt, validate_jwt}; + use crate::utils::time_utils::time; + use serde::{Deserialize, Serialize}; + + #[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] + pub struct Claims { + sub: String, + exp: u64, + } + + #[test] + fn jwt_encode_sign_verify_valid() { + let (pub_key, priv_key) = generate_key_pair().unwrap(); + let claims = Claims { + sub: "my-sub".to_string(), + exp: time() + 100, + }; + let jwt = sign_jwt(&priv_key, &claims).expect("Failed to sign JWT!"); + + println!("pub {pub_key:?}"); + println!("priv {priv_key:?}"); + let claims_out = validate_jwt(&pub_key, &jwt).expect("Failed to validate JWT!"); + + assert_eq!(claims, claims_out) + } +} diff --git a/virtweb_backend/src/utils/mod.rs b/virtweb_backend/src/utils/mod.rs index f10b3e2..2c0528e 100644 --- a/virtweb_backend/src/utils/mod.rs +++ b/virtweb_backend/src/utils/mod.rs @@ -1,5 +1,6 @@ pub mod disks_utils; pub mod files_utils; +pub mod jwt_utils; pub mod net_utils; pub mod rand_utils; pub mod time_utils; -- 2.45.2 From 0217d1c53de95bf06edbc17c24616d662816a082 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 8 Apr 2024 22:46:17 +0200 Subject: [PATCH 02/36] WIP --- virtweb_backend/Cargo.lock | 193 ++++++++++++++++++++++++- virtweb_backend/Cargo.toml | 4 +- virtweb_backend/src/utils/jwt_utils.rs | 35 ++--- 3 files changed, 206 insertions(+), 26 deletions(-) diff --git a/virtweb_backend/Cargo.lock b/virtweb_backend/Cargo.lock index 18c6836..43860b9 100644 --- a/virtweb_backend/Cargo.lock +++ b/virtweb_backend/Cargo.lock @@ -568,6 +568,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.20.0" @@ -586,6 +592,12 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bincode" version = "2.0.0-rc.3" @@ -792,6 +804,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "convert_case" version = "0.4.0" @@ -890,6 +908,18 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -945,6 +975,17 @@ dependencies = [ "syn 2.0.58", ] +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.3.11" @@ -974,6 +1015,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -984,12 +1026,47 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + [[package]] name = "either" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "encoding_rs" version = "0.8.33" @@ -1069,6 +1146,16 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core", + "subtle", +] + [[package]] name = "flate2" version = "1.0.28" @@ -1215,6 +1302,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1256,6 +1344,17 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + [[package]] name = "h2" version = "0.3.26" @@ -2024,6 +2123,18 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "p384" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -2069,6 +2180,15 @@ dependencies = [ "serde", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -2107,6 +2227,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.30" @@ -2150,6 +2280,15 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro2" version = "1.0.79" @@ -2394,6 +2533,16 @@ dependencies = [ "winreg", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "rgb" version = "0.8.37" @@ -2526,6 +2675,20 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "2.10.0" @@ -2647,6 +2810,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -2708,6 +2881,16 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "strsim" version = "0.10.0" @@ -3192,6 +3375,7 @@ dependencies = [ "bytes", "clap", "dotenvy", + "elliptic-curve", "env_logger", "futures", "futures-util", @@ -3205,11 +3389,10 @@ dependencies = [ "mime_guess", "nix", "num", - "pem", + "p384", "quick-xml", "rand", "reqwest", - "ring", "rust-embed", "serde", "serde_json", @@ -3563,6 +3746,12 @@ dependencies = [ "syn 2.0.58", ] +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" + [[package]] name = "zstd" version = "0.13.1" diff --git a/virtweb_backend/Cargo.toml b/virtweb_backend/Cargo.toml index 08e7383..a7c045e 100644 --- a/virtweb_backend/Cargo.toml +++ b/virtweb_backend/Cargo.toml @@ -46,5 +46,5 @@ mime_guess = "2.0.4" dotenvy = "0.15.7" nix = { version = "0.28.0", features = ["net"] } jsonwebtoken = "9.3.0" -ring = "0.17.8" -pem = "3.0.4" \ No newline at end of file +elliptic-curve = { version = "0.13.8", features = ["pkcs8","pem" ] } +p384 = { version = "0.13.0", features = ["ecdsa", "pkcs8", "pem"] } \ No newline at end of file diff --git a/virtweb_backend/src/utils/jwt_utils.rs b/virtweb_backend/src/utils/jwt_utils.rs index 67819c0..813f81c 100644 --- a/virtweb_backend/src/utils/jwt_utils.rs +++ b/virtweb_backend/src/utils/jwt_utils.rs @@ -1,5 +1,8 @@ +use elliptic_curve::pkcs8::EncodePublicKey; use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Validation}; -use ring::signature::{KeyPair, UnparsedPublicKey}; +use p384::ecdsa::{SigningKey, VerifyingKey}; +use p384::pkcs8::{EncodePrivateKey, LineEnding}; +use rand::rngs::OsRng; use serde::de::DeserializeOwned; use serde::Serialize; @@ -23,25 +26,14 @@ pub enum TokenPrivKey { /// Generate a new token keypair pub fn generate_key_pair() -> anyhow::Result<(TokenPubKey, TokenPrivKey)> { - let doc = ring::signature::EcdsaKeyPair::generate_pkcs8( - &ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING, - &ring::rand::SystemRandom::new(), - )?; + let signing_key = SigningKey::random(&mut OsRng); + let priv_pem = signing_key + .to_pkcs8_der()? + .to_pem("PRIVATE KEY", LineEnding::LF)? + .to_string(); - let priv_pem = pem::encode(&pem::Pem::new("PRIVATE KEY", doc.as_ref())); - - let pair = ring::signature::EcdsaKeyPair::from_pkcs8( - &ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING, - doc.as_ref(), - &ring::rand::SystemRandom::new(), - )?; - let pub_pem = pem::encode(&pem::Pem::new("PUBLIC KEY", pair.public_key().as_ref())); - - - let pk = pair.public_key(); - let unp = UnparsedPublicKey::new(&ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING, pk.as_ref()); - - let decoding_key = DecodingKey::from_ec_pem(pub_pem.as_bytes()).expect("aie ai"); + let pub_key = VerifyingKey::from(signing_key); + let pub_pem = pub_key.to_public_key_pem(LineEnding::LF)?; Ok(( TokenPubKey::ES384 { r#pub: pub_pem }, @@ -99,11 +91,10 @@ mod test { exp: time() + 100, }; let jwt = sign_jwt(&priv_key, &claims).expect("Failed to sign JWT!"); - - println!("pub {pub_key:?}"); - println!("priv {priv_key:?}"); let claims_out = validate_jwt(&pub_key, &jwt).expect("Failed to validate JWT!"); assert_eq!(claims, claims_out) } + + // TODO : complete tests } -- 2.45.2 From bab34b7c7ff5b2a35799920670f1603b032b4547 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 9 Apr 2024 18:04:00 +0200 Subject: [PATCH 03/36] Add more test to validate JWTs --- virtweb_backend/src/utils/jwt_utils.rs | 50 +++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/virtweb_backend/src/utils/jwt_utils.rs b/virtweb_backend/src/utils/jwt_utils.rs index 813f81c..aa379bc 100644 --- a/virtweb_backend/src/utils/jwt_utils.rs +++ b/virtweb_backend/src/utils/jwt_utils.rs @@ -83,18 +83,58 @@ mod test { exp: u64, } + impl Default for Claims { + fn default() -> Self { + Self { + sub: "my-sub".to_string(), + exp: time() + 100, + } + } + } + #[test] fn jwt_encode_sign_verify_valid() { let (pub_key, priv_key) = generate_key_pair().unwrap(); - let claims = Claims { - sub: "my-sub".to_string(), - exp: time() + 100, - }; + let claims = Claims::default(); let jwt = sign_jwt(&priv_key, &claims).expect("Failed to sign JWT!"); let claims_out = validate_jwt(&pub_key, &jwt).expect("Failed to validate JWT!"); assert_eq!(claims, claims_out) } - // TODO : complete tests + #[test] + fn jwt_encode_sign_verify_invalid_key() { + let (_pub_key, priv_key) = generate_key_pair().unwrap(); + let (pub_key_2, _priv_key_2) = generate_key_pair().unwrap(); + let claims = Claims::default(); + let jwt = sign_jwt(&priv_key, &claims).expect("Failed to sign JWT!"); + validate_jwt::(&pub_key_2, &jwt).expect_err("JWT should not have validated!"); + } + + #[test] + fn jwt_verify_random_string() { + let (pub_key, _priv_key) = generate_key_pair().unwrap(); + validate_jwt::(&pub_key, "random_string") + .expect_err("JWT should not have validated!"); + } + + #[test] + fn jwt_expired() { + let (pub_key, priv_key) = generate_key_pair().unwrap(); + let claims = Claims { + exp: time() - 100, + ..Default::default() + }; + let jwt = sign_jwt(&priv_key, &claims).expect("Failed to sign JWT!"); + validate_jwt::(&pub_key, &jwt).expect_err("JWT should not have validated!"); + } + + #[test] + fn jwt_invalid_signature() { + let (pub_key, priv_key) = generate_key_pair().unwrap(); + let claims = Claims::default(); + let jwt = sign_jwt(&priv_key, &claims).expect("Failed to sign JWT!"); + validate_jwt::(&pub_key, &format!("{jwt}bad")) + .expect_err("JWT should not have validated!"); + } } -- 2.45.2 From 60a3cb3d10da4dbc1a5b397b6249321e5d10f647 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 9 Apr 2024 18:36:18 +0200 Subject: [PATCH 04/36] Can create API tokens --- virtweb_backend/src/api_tokens.rs | 31 +++++++++++++++++++++---------- virtweb_backend/src/app_config.rs | 5 +++++ 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/virtweb_backend/src/api_tokens.rs b/virtweb_backend/src/api_tokens.rs index 8ec405b..45af62e 100644 --- a/virtweb_backend/src/api_tokens.rs +++ b/virtweb_backend/src/api_tokens.rs @@ -1,5 +1,6 @@ //! # API tokens management +use crate::app_config::AppConfig; use crate::constants; use crate::utils::jwt_utils; use crate::utils::jwt_utils::{TokenPrivKey, TokenPubKey}; @@ -10,9 +11,9 @@ pub struct TokenID(pub uuid::Uuid); #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] pub struct Token { + pub id: TokenID, pub name: String, pub description: String, - pub id: TokenID, created: u64, updated: u64, pub pub_key: TokenPubKey, @@ -22,6 +23,13 @@ pub struct Token { pub delete_after_inactivity: Option, } +impl Token { + /// Turn the token into a JSON string + pub fn to_json(&self) -> String { + serde_json::to_string(self).expect("Failed to serialize an API token!") + } +} + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, Eq, PartialEq)] pub enum TokenVerb { GET, @@ -83,23 +91,26 @@ impl NewToken { } /// Create a new Token -pub async fn create(token: &NewToken) -> anyhow::Result<(Token, TokenPrivKey)> { +pub async fn create(t: &NewToken) -> anyhow::Result<(Token, TokenPrivKey)> { let (pub_key, priv_key) = jwt_utils::generate_key_pair()?; - let full_token = Token { - name: token.name.to_string(), - description: token.description.to_string(), + 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, - rights: token.rights.clone(), + rights: t.rights.clone(), last_used: Some(time()), - ip_restriction: token.ip_restriction, - delete_after_inactivity: token.delete_after_inactivity, + ip_restriction: t.ip_restriction, + delete_after_inactivity: t.delete_after_inactivity, }; - // TODO : save + std::fs::write( + AppConfig::get().api_token_definition_path(token.id), + token.to_json(), + )?; - Ok((full_token, priv_key)) + Ok((token, priv_key)) } diff --git a/virtweb_backend/src/app_config.rs b/virtweb_backend/src/app_config.rs index bfa577e..16780b6 100644 --- a/virtweb_backend/src/app_config.rs +++ b/virtweb_backend/src/app_config.rs @@ -1,3 +1,4 @@ +use crate::api_tokens::TokenID; use crate::constants; use crate::libvirt_lib_structures::XMLUuid; use crate::libvirt_rest_structures::net::NetworkName; @@ -272,6 +273,10 @@ impl AppConfig { pub fn api_tokens_path(&self) -> PathBuf { self.storage_path().join(constants::STORAGE_TOKENS_DIR) } + + pub fn api_token_definition_path(&self, id: TokenID) -> PathBuf { + self.api_tokens_path().join(format!("{}.json", id.0)) + } } #[derive(Debug, Clone, serde::Serialize)] -- 2.45.2 From f56e9c14b2727c3b01ec103c24e0f108f4c1d339 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 9 Apr 2024 18:56:12 +0200 Subject: [PATCH 05/36] Can get the list of tokens --- virtweb_backend/src/api_tokens.rs | 15 +++++++++++++++ .../src/controllers/api_tokens_controller.rs | 11 +++++++++++ virtweb_backend/src/main.rs | 4 ++-- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/virtweb_backend/src/api_tokens.rs b/virtweb_backend/src/api_tokens.rs index 45af62e..6e91d0c 100644 --- a/virtweb_backend/src/api_tokens.rs +++ b/virtweb_backend/src/api_tokens.rs @@ -5,6 +5,7 @@ use crate::constants; use crate::utils::jwt_utils; use crate::utils::jwt_utils::{TokenPrivKey, TokenPubKey}; use crate::utils::time_utils::time; +use std::path::Path; #[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)] pub struct TokenID(pub uuid::Uuid); @@ -28,6 +29,11 @@ impl Token { pub fn to_json(&self) -> String { serde_json::to_string(self).expect("Failed to serialize an API token!") } + + /// Load token information from a file + pub fn load_from_file(path: &Path) -> anyhow::Result { + Ok(serde_json::from_str(&std::fs::read_to_string(path)?)?) + } } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, Eq, PartialEq)] @@ -114,3 +120,12 @@ pub async fn create(t: &NewToken) -> anyhow::Result<(Token, TokenPrivKey)> { Ok((token, priv_key)) } + +/// Get the entire list of api toksn +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) +} diff --git a/virtweb_backend/src/controllers/api_tokens_controller.rs b/virtweb_backend/src/controllers/api_tokens_controller.rs index e36a6b7..12da9f6 100644 --- a/virtweb_backend/src/controllers/api_tokens_controller.rs +++ b/virtweb_backend/src/controllers/api_tokens_controller.rs @@ -47,3 +47,14 @@ pub async fn create(new_token: web::Json) -> HttpResult { priv_key, })) } + +/// Get the list of API tokens +pub async fn list() -> HttpResult { + let list = api_tokens::full_list() + .await? + .into_iter() + .map(RestToken::new) + .collect::>(); + + Ok(HttpResponse::Ok().json(list)) +} diff --git a/virtweb_backend/src/main.rs b/virtweb_backend/src/main.rs index c2db550..7b84199 100644 --- a/virtweb_backend/src/main.rs +++ b/virtweb_backend/src/main.rs @@ -282,11 +282,11 @@ async fn main() -> std::io::Result<()> { "/api/tokens/create", web::post().to(api_tokens_controller::create), ) - /* TODO .route( + .route( "/api/tokens/list", web::get().to(api_tokens_controller::list), ) - .route( + /* TODO .route( "/api/tokens/{uid}", web::get().to(api_tokens_controller::get_single), ) -- 2.45.2 From 0c5a232a25ec11350ba59e0fe0b9bc2369e221a8 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 9 Apr 2024 19:04:49 +0200 Subject: [PATCH 06/36] Can get a single API token --- virtweb_backend/src/api_tokens.rs | 8 +++++++- .../src/controllers/api_tokens_controller.rs | 14 +++++++++++++- virtweb_backend/src/main.rs | 4 ++-- virtweb_backend/src/utils/jwt_utils.rs | 10 ++++++++-- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/virtweb_backend/src/api_tokens.rs b/virtweb_backend/src/api_tokens.rs index 6e91d0c..3222c9a 100644 --- a/virtweb_backend/src/api_tokens.rs +++ b/virtweb_backend/src/api_tokens.rs @@ -17,6 +17,7 @@ pub struct Token { pub description: String, created: u64, updated: u64, + #[serde(skip_serializing_if = "TokenPubKey::is_invalid")] pub pub_key: TokenPubKey, pub rights: Vec, pub last_used: Option, @@ -121,7 +122,7 @@ pub async fn create(t: &NewToken) -> anyhow::Result<(Token, TokenPrivKey)> { Ok((token, priv_key)) } -/// Get the entire list of api toksn +/// 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())? { @@ -129,3 +130,8 @@ pub async fn full_list() -> anyhow::Result> { } 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)) +} diff --git a/virtweb_backend/src/controllers/api_tokens_controller.rs b/virtweb_backend/src/controllers/api_tokens_controller.rs index 12da9f6..4f80eae 100644 --- a/virtweb_backend/src/controllers/api_tokens_controller.rs +++ b/virtweb_backend/src/controllers/api_tokens_controller.rs @@ -1,7 +1,7 @@ //! # API tokens management use crate::api_tokens; -use crate::api_tokens::NewToken; +use crate::api_tokens::{NewToken, TokenID}; use crate::controllers::api_tokens_controller::rest_token::RestToken; use crate::controllers::HttpResult; use crate::utils::jwt_utils::TokenPrivKey; @@ -58,3 +58,15 @@ pub async fn list() -> HttpResult { Ok(HttpResponse::Ok().json(list)) } + +#[derive(serde::Deserialize)] +pub struct TokenIDInPath { + uid: TokenID, +} + +/// Get the information about a single token +pub async fn get_single(path: web::Path) -> HttpResult { + let token = api_tokens::get_single(path.uid).await?; + + Ok(HttpResponse::Ok().json(RestToken::new(token))) +} diff --git a/virtweb_backend/src/main.rs b/virtweb_backend/src/main.rs index 7b84199..4176739 100644 --- a/virtweb_backend/src/main.rs +++ b/virtweb_backend/src/main.rs @@ -286,11 +286,11 @@ async fn main() -> std::io::Result<()> { "/api/tokens/list", web::get().to(api_tokens_controller::list), ) - /* TODO .route( + .route( "/api/tokens/{uid}", web::get().to(api_tokens_controller::get_single), ) - .route( + /* TODO .route( "/api/tokens/{uid}", web::put().to(api_tokens_controller::update), ) diff --git a/virtweb_backend/src/utils/jwt_utils.rs b/virtweb_backend/src/utils/jwt_utils.rs index aa379bc..d8beb25 100644 --- a/virtweb_backend/src/utils/jwt_utils.rs +++ b/virtweb_backend/src/utils/jwt_utils.rs @@ -6,10 +6,10 @@ use rand::rngs::OsRng; use serde::de::DeserializeOwned; use serde::Serialize; -#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Eq, PartialEq)] #[serde(tag = "alg")] pub enum TokenPubKey { - /// This variant DOES make crash the program. It MUST NOT be serialized. + /// This variant DOES make crash the program. It MUST NOT used to validate JWT. /// /// It is a hack to hide public key when getting the list of tokens None, @@ -18,6 +18,12 @@ pub enum TokenPubKey { ES384 { r#pub: String }, } +impl TokenPubKey { + pub fn is_invalid(&self) -> bool { + self == &TokenPubKey::None + } +} + #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] #[serde(tag = "alg")] pub enum TokenPrivKey { -- 2.45.2 From 0ee2c41f3cc2d3c213f573013911763706fe73e2 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 9 Apr 2024 19:38:09 +0200 Subject: [PATCH 07/36] Can update and delete API tokens --- virtweb_backend/src/api_tokens.rs | 67 +++++++++++++------ .../src/controllers/api_tokens_controller.rs | 30 ++++++++- virtweb_backend/src/main.rs | 6 +- 3 files changed, 80 insertions(+), 23 deletions(-) diff --git a/virtweb_backend/src/api_tokens.rs b/virtweb_backend/src/api_tokens.rs index 3222c9a..9d8bab3 100644 --- a/virtweb_backend/src/api_tokens.rs +++ b/virtweb_backend/src/api_tokens.rs @@ -10,6 +10,15 @@ use std::path::Path; #[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)] pub struct TokenID(pub uuid::Uuid); +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct TokenRight { + verb: TokenVerb, + uri: String, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct TokenRights(Vec); + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] pub struct Token { pub id: TokenID, @@ -19,7 +28,7 @@ pub struct Token { updated: u64, #[serde(skip_serializing_if = "TokenPubKey::is_invalid")] pub pub_key: TokenPubKey, - pub rights: Vec, + pub rights: TokenRights, pub last_used: Option, pub ip_restriction: Option, pub delete_after_inactivity: Option, @@ -27,8 +36,12 @@ pub struct Token { impl Token { /// Turn the token into a JSON string - pub fn to_json(&self) -> String { - serde_json::to_string(self).expect("Failed to serialize an API token!") + 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 @@ -46,22 +59,27 @@ pub enum TokenVerb { DELETE, } -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct TokenRights { - verb: TokenVerb, - uri: String, -} - /// Structure used to create a token #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] pub struct NewToken { pub name: String, pub description: String, - pub rights: Vec, + pub rights: TokenRights, pub ip_restriction: Option, pub delete_after_inactivity: Option, } +impl TokenRights { + pub fn check_error(&self) -> Option<&'static str> { + for r in &self.0 { + if !r.uri.starts_with("/api/") { + return Some("All API rights shall start with /api/"); + } + } + None + } +} + impl NewToken { /// Check for error in token pub fn check_error(&self) -> Option<&'static str> { @@ -81,10 +99,8 @@ impl NewToken { return Some("Description is too long!"); } - for r in &self.rights { - if !r.uri.starts_with("/api/") { - return Some("All API rights shall start with /api/"); - } + if let Some(err) = self.rights.check_error() { + return Some(err); } if let Some(t) = self.delete_after_inactivity { @@ -114,11 +130,6 @@ pub async fn create(t: &NewToken) -> anyhow::Result<(Token, TokenPrivKey)> { delete_after_inactivity: t.delete_after_inactivity, }; - std::fs::write( - AppConfig::get().api_token_definition_path(token.id), - token.to_json(), - )?; - Ok((token, priv_key)) } @@ -135,3 +146,21 @@ pub async fn full_list() -> anyhow::Result> { 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(()) +} + +/// 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(()) +} diff --git a/virtweb_backend/src/controllers/api_tokens_controller.rs b/virtweb_backend/src/controllers/api_tokens_controller.rs index 4f80eae..3f40161 100644 --- a/virtweb_backend/src/controllers/api_tokens_controller.rs +++ b/virtweb_backend/src/controllers/api_tokens_controller.rs @@ -1,7 +1,7 @@ //! # API tokens management use crate::api_tokens; -use crate::api_tokens::{NewToken, TokenID}; +use crate::api_tokens::{NewToken, TokenID, TokenRights}; use crate::controllers::api_tokens_controller::rest_token::RestToken; use crate::controllers::HttpResult; use crate::utils::jwt_utils::TokenPrivKey; @@ -70,3 +70,31 @@ pub async fn get_single(path: web::Path) -> HttpResult { Ok(HttpResponse::Ok().json(RestToken::new(token))) } + +#[derive(serde::Deserialize)] +pub struct UpdateTokenBody { + rights: TokenRights, +} + +/// Update a token +pub async fn update( + path: web::Path, + body: web::Json, +) -> HttpResult { + if let Some(err) = body.rights.check_error() { + log::error!("Failed to validate updated API token information! {err}"); + return Ok(HttpResponse::BadRequest() + .json(format!("Failed to validate API token information! {err}"))); + } + + api_tokens::update_rights(path.uid, body.0.rights).await?; + + Ok(HttpResponse::Accepted().finish()) +} + +/// Delete a token +pub async fn delete(path: web::Path) -> HttpResult { + api_tokens::delete(path.uid).await?; + + Ok(HttpResponse::Accepted().finish()) +} diff --git a/virtweb_backend/src/main.rs b/virtweb_backend/src/main.rs index 4176739..a968342 100644 --- a/virtweb_backend/src/main.rs +++ b/virtweb_backend/src/main.rs @@ -290,14 +290,14 @@ async fn main() -> std::io::Result<()> { "/api/tokens/{uid}", web::get().to(api_tokens_controller::get_single), ) - /* TODO .route( + .route( "/api/tokens/{uid}", - web::put().to(api_tokens_controller::update), + web::patch().to(api_tokens_controller::update), ) .route( "/api/tokens/{uid}", web::delete().to(api_tokens_controller::delete), - )*/ + ) // Static assets .route("/", web::get().to(static_controller::root_index)) .route( -- 2.45.2 From 418055a640db08066e5e8008075b564bd4d8a088 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 9 Apr 2024 19:41:33 +0200 Subject: [PATCH 08/36] Forgot to save new tokens... --- virtweb_backend/src/api_tokens.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/virtweb_backend/src/api_tokens.rs b/virtweb_backend/src/api_tokens.rs index 9d8bab3..91aef97 100644 --- a/virtweb_backend/src/api_tokens.rs +++ b/virtweb_backend/src/api_tokens.rs @@ -130,6 +130,8 @@ pub async fn create(t: &NewToken) -> anyhow::Result<(Token, TokenPrivKey)> { delete_after_inactivity: t.delete_after_inactivity, }; + token.save()?; + Ok((token, priv_key)) } -- 2.45.2 From fd3df3d214614d8e8de0faa8c425eb868924626b Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 9 Apr 2024 21:49:16 +0200 Subject: [PATCH 09/36] Start to implement API tokens checks --- virtweb_backend/src/api_tokens.rs | 46 ++++++- .../src/extractors/api_auth_extractor.rs | 123 ++++++++++++++++++ virtweb_backend/src/extractors/mod.rs | 1 + .../src/middlewares/auth_middleware.rs | 25 +++- 4 files changed, 188 insertions(+), 7 deletions(-) create mode 100644 virtweb_backend/src/extractors/api_auth_extractor.rs diff --git a/virtweb_backend/src/api_tokens.rs b/virtweb_backend/src/api_tokens.rs index 91aef97..f216974 100644 --- a/virtweb_backend/src/api_tokens.rs +++ b/virtweb_backend/src/api_tokens.rs @@ -5,11 +5,19 @@ use crate::constants; use crate::utils::jwt_utils; use crate::utils::jwt_utils::{TokenPrivKey, TokenPubKey}; use crate::utils::time_utils::time; +use actix_http::Method; use std::path::Path; #[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)] pub struct TokenRight { verb: TokenVerb, @@ -29,9 +37,9 @@ pub struct Token { #[serde(skip_serializing_if = "TokenPubKey::is_invalid")] pub pub_key: TokenPubKey, pub rights: TokenRights, - pub last_used: Option, + pub last_used: u64, pub ip_restriction: Option, - pub delete_after_inactivity: Option, + pub max_inactivity: Option, } impl Token { @@ -45,9 +53,25 @@ impl Token { } /// Load token information from a file - pub fn load_from_file(path: &Path) -> anyhow::Result { + 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)] @@ -59,6 +83,18 @@ pub enum TokenVerb { 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, + } + } +} + /// Structure used to create a token #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] pub struct NewToken { @@ -125,9 +161,9 @@ pub async fn create(t: &NewToken) -> anyhow::Result<(Token, TokenPrivKey)> { updated: time(), pub_key, rights: t.rights.clone(), - last_used: Some(time()), + last_used: time(), ip_restriction: t.ip_restriction, - delete_after_inactivity: t.delete_after_inactivity, + max_inactivity: t.delete_after_inactivity, }; token.save()?; diff --git a/virtweb_backend/src/extractors/api_auth_extractor.rs b/virtweb_backend/src/extractors/api_auth_extractor.rs new file mode 100644 index 0000000..bed5da3 --- /dev/null +++ b/virtweb_backend/src/extractors/api_auth_extractor.rs @@ -0,0 +1,123 @@ +use crate::api_tokens::{Token, TokenID, TokenVerb}; + +use crate::api_tokens; +use crate::utils::jwt_utils; +use crate::utils::time_utils::time; +use actix_web::dev::Payload; +use actix_web::error::ErrorBadRequest; +use actix_web::{Error, FromRequest, HttpRequest}; +use std::future::Future; +use std::pin::Pin; + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +pub struct TokenClaims { + pub sub: String, + pub iat: usize, + pub exp: usize, + pub verb: TokenVerb, + pub path: String, + pub nonce: String, +} + +pub struct ApiAuthExtractor { + pub token: Token, + pub claims: TokenClaims, +} + +impl FromRequest for ApiAuthExtractor { + type Error = Error; + type Future = Pin>>>; + + fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future { + let req = req.clone(); + + Box::pin(async move { + let (token_id, token_jwt) = match ( + req.headers().get("x-token-id"), + req.headers().get("x-token-content"), + ) { + (Some(id), Some(jwt)) => ( + id.to_str().unwrap_or("").to_string(), + jwt.to_str().unwrap_or("").to_string(), + ), + (_, _) => { + return Err(ErrorBadRequest("API auth headers were not all specified!")); + } + }; + + let token_id = match TokenID::parse(&token_id) { + Ok(t) => t, + Err(e) => { + log::error!("Failed to parse token id! {e}"); + return Err(ErrorBadRequest("Unable to validate token ID!")); + } + }; + + let token = match api_tokens::get_single(token_id).await { + Ok(t) => t, + Err(e) => { + log::error!("Failed to retrieve token: {e}"); + return Err(ErrorBadRequest("Unable to validate token!")); + } + }; + + if token.is_expired() { + log::error!("Token has expired (not been used for too long)!"); + return Err(ErrorBadRequest("Unable to validate token!")); + } + + let claims = match jwt_utils::validate_jwt::(&token.pub_key, &token_jwt) { + Ok(c) => c, + Err(e) => { + log::error!("Failed to validate JWT: {e}"); + return Err(ErrorBadRequest("Unable to validate token!")); + } + }; + + if claims.sub != token.id.0.to_string() { + log::error!("JWT sub mismatch (should equal to token id)!"); + return Err(ErrorBadRequest( + "JWT sub mismatch (should equal to token id)!", + )); + } + + if time() + 60 * 15 < claims.iat as u64 { + log::error!("iat is in the future!"); + return Err(ErrorBadRequest("iat is in the future!")); + } + + if claims.exp < claims.iat { + log::error!("exp shall not be smaller than iat!"); + return Err(ErrorBadRequest("exp shall not be smaller than iat!")); + } + + if claims.exp - claims.iat > 1800 { + log::error!("JWT shall not be valid more than 30 minutes!"); + return Err(ErrorBadRequest( + "JWT shall not be valid more than 30 minutes!", + )); + } + + if claims.path != req.path() { + log::error!("JWT path mismatch!"); + return Err(ErrorBadRequest("JWT path mismatch!")); + } + + if claims.verb.as_method() != req.method() { + log::error!("JWT method mismatch!"); + return Err(ErrorBadRequest("JWT method mismatch!")); + } + + // TODO : check if route is authorized with token + // TODO : check for ip restriction + + // TODO : manually validate all checks + + if token.should_update_last_activity() { + // TODO : update last activity + } + + Ok(ApiAuthExtractor { token, claims }) + }) + } +} diff --git a/virtweb_backend/src/extractors/mod.rs b/virtweb_backend/src/extractors/mod.rs index d7e49e1..d284d19 100644 --- a/virtweb_backend/src/extractors/mod.rs +++ b/virtweb_backend/src/extractors/mod.rs @@ -1,2 +1,3 @@ +pub mod api_auth_extractor; pub mod auth_extractor; pub mod local_auth_extractor; diff --git a/virtweb_backend/src/middlewares/auth_middleware.rs b/virtweb_backend/src/middlewares/auth_middleware.rs index f279216..7479b4d 100644 --- a/virtweb_backend/src/middlewares/auth_middleware.rs +++ b/virtweb_backend/src/middlewares/auth_middleware.rs @@ -3,6 +3,7 @@ use std::rc::Rc; use crate::app_config::AppConfig; use crate::constants; +use crate::extractors::api_auth_extractor::ApiAuthExtractor; use crate::extractors::auth_extractor::AuthExtractor; use actix_web::body::EitherBody; use actix_web::dev::Payload; @@ -68,8 +69,28 @@ where let auth_disabled = AppConfig::get().unsecure_disable_auth; - // Check authentication, if required - if !auth_disabled + // Check API authentication + if req.headers().get("x-token-id").is_some() { + let auth = + match ApiAuthExtractor::from_request(req.request(), &mut Payload::None).await { + Ok(auth) => auth, + Err(e) => { + log::error!( + "Failed to extract API authentication information from request! {e}" + ); + return Ok(req + .into_response(HttpResponse::PreconditionFailed().finish()) + .map_into_right_body()); + } + }; + + log::info!( + "Using API token '{}' to perform the request", + auth.token.name + ); + } + // Check user authentication, if required + else if !auth_disabled && !constants::ROUTES_WITHOUT_AUTH.contains(&req.path()) && req.path().starts_with("/api/") { -- 2.45.2 From 631cc9653739637dfc32ba5e02224b8257170ba1 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 9 Apr 2024 21:53:11 +0200 Subject: [PATCH 10/36] Give more appropriate name to define authorized route for API token --- virtweb_backend/src/api_tokens.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/virtweb_backend/src/api_tokens.rs b/virtweb_backend/src/api_tokens.rs index f216974..f71342d 100644 --- a/virtweb_backend/src/api_tokens.rs +++ b/virtweb_backend/src/api_tokens.rs @@ -21,7 +21,7 @@ impl TokenID { #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] pub struct TokenRight { verb: TokenVerb, - uri: String, + path: String, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] @@ -108,7 +108,7 @@ pub struct NewToken { impl TokenRights { pub fn check_error(&self) -> Option<&'static str> { for r in &self.0 { - if !r.uri.starts_with("/api/") { + if !r.path.starts_with("/api/") { return Some("All API rights shall start with /api/"); } } -- 2.45.2 From b1937d42a2781840b3565f73370a12e2529f8d88 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 10 Apr 2024 21:03:05 +0200 Subject: [PATCH 11/36] Create a basic API client --- virtweb_backend/examples/api_curl.rs | 66 ++++++++++++++++++++++++++++ virtweb_backend/src/api_tokens.rs | 16 +++++++ 2 files changed, 82 insertions(+) create mode 100644 virtweb_backend/examples/api_curl.rs diff --git a/virtweb_backend/examples/api_curl.rs b/virtweb_backend/examples/api_curl.rs new file mode 100644 index 0000000..cf363b8 --- /dev/null +++ b/virtweb_backend/examples/api_curl.rs @@ -0,0 +1,66 @@ +use clap::Parser; +use std::os::unix::prelude::CommandExt; +use std::process::Command; +use std::str::FromStr; +use virtweb_backend::api_tokens::TokenVerb; +use virtweb_backend::extractors::api_auth_extractor::TokenClaims; +use virtweb_backend::utils::jwt_utils::{sign_jwt, TokenPrivKey}; +use virtweb_backend::utils::time_utils::time; + +/// curl wrapper to query Virtweb backend API +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + /// URL of VirtWeb + #[arg(short('u'), long, env, default_value = "http://localhost:8000")] + virtweb_url: String, + + /// Token ID + #[arg(short('i'), long, env)] + token_id: String, + + /// Token private key + #[arg(short('t'), long, env)] + token_key: String, + + /// Request verb + #[arg(short('X'), long, default_value = "GET")] + verb: String, + + /// Request URI + uri: String, + + /// Command line arguments to pass to cURL + #[clap(trailing_var_arg = true, allow_hyphen_values = true)] + run: Vec, +} + +fn main() { + let args = Args::parse(); + + let full_url = format!("{}{}", args.virtweb_url, args.uri); + log::debug!("Full URL: {full_url}"); + + let key = TokenPrivKey::ES384 { + r#priv: args.token_key, + }; + let claims = TokenClaims { + sub: args.token_id.to_string(), + iat: time() as usize, + exp: time() as usize + 50, + verb: TokenVerb::from_str(&args.verb).expect("Invalid request verb!"), + path: args.uri, + nonce: uuid::Uuid::new_v4().to_string(), + }; + + let jwt = sign_jwt(&key, &claims).expect("Failed to sign JWT!"); + + Command::new("curl") + .args(["-X", &args.verb]) + .args(["-H", &format!("x-token-id: {}", args.token_id)]) + .args(["-H", &format!("x-token-content: {jwt}")]) + .args(args.run) + .arg(full_url) + .exec(); + panic!("Failed to run curl!") +} diff --git a/virtweb_backend/src/api_tokens.rs b/virtweb_backend/src/api_tokens.rs index f71342d..31dacdc 100644 --- a/virtweb_backend/src/api_tokens.rs +++ b/virtweb_backend/src/api_tokens.rs @@ -7,6 +7,7 @@ use crate::utils::jwt_utils::{TokenPrivKey, TokenPubKey}; use crate::utils::time_utils::time; use actix_http::Method; use std::path::Path; +use std::str::FromStr; #[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)] pub struct TokenID(pub uuid::Uuid); @@ -95,6 +96,21 @@ impl TokenVerb { } } +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 { -- 2.45.2 From 579e54f7d31deb9ad1dca76a2e0ff1526b226511 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 10 Apr 2024 21:10:00 +0200 Subject: [PATCH 12/36] Update last activity of token --- virtweb_backend/src/api_tokens.rs | 8 ++++++++ virtweb_backend/src/extractors/api_auth_extractor.rs | 5 ++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/virtweb_backend/src/api_tokens.rs b/virtweb_backend/src/api_tokens.rs index 31dacdc..c2955d7 100644 --- a/virtweb_backend/src/api_tokens.rs +++ b/virtweb_backend/src/api_tokens.rs @@ -210,6 +210,14 @@ pub async fn update_rights(id: TokenID, rights: TokenRights) -> anyhow::Result<( 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); diff --git a/virtweb_backend/src/extractors/api_auth_extractor.rs b/virtweb_backend/src/extractors/api_auth_extractor.rs index bed5da3..604e2e4 100644 --- a/virtweb_backend/src/extractors/api_auth_extractor.rs +++ b/virtweb_backend/src/extractors/api_auth_extractor.rs @@ -114,7 +114,10 @@ impl FromRequest for ApiAuthExtractor { // TODO : manually validate all checks if token.should_update_last_activity() { - // TODO : update last activity + if let Err(e) = api_tokens::refresh_last_used(token.id).await { + log::error!("Could not update token last activity! {e}"); + return Err(ErrorBadRequest("!")); + } } Ok(ApiAuthExtractor { token, claims }) -- 2.45.2 From d5eee04d7ac6430719b6c9ed1549befa8a35f9f1 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 10 Apr 2024 21:22:15 +0200 Subject: [PATCH 13/36] Can block token unauthorized to access a specific route --- virtweb_backend/src/api_tokens.rs | 31 ++++++++++++------- .../src/extractors/api_auth_extractor.rs | 15 +++++++-- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/virtweb_backend/src/api_tokens.rs b/virtweb_backend/src/api_tokens.rs index c2955d7..de2591c 100644 --- a/virtweb_backend/src/api_tokens.rs +++ b/virtweb_backend/src/api_tokens.rs @@ -19,7 +19,7 @@ impl TokenID { } } -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq)] pub struct TokenRight { verb: TokenVerb, path: String, @@ -28,6 +28,24 @@ pub struct TokenRight { #[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/"); + } + } + None + } + + pub fn contains(&self, verb: TokenVerb, path: &str) -> bool { + self.0.contains(&TokenRight { + verb, + path: path.to_string(), + }) + } +} + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] pub struct Token { pub id: TokenID, @@ -121,17 +139,6 @@ pub struct NewToken { pub delete_after_inactivity: Option, } -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/"); - } - } - None - } -} - impl NewToken { /// Check for error in token pub fn check_error(&self) -> Option<&'static str> { diff --git a/virtweb_backend/src/extractors/api_auth_extractor.rs b/virtweb_backend/src/extractors/api_auth_extractor.rs index 604e2e4..0f22043 100644 --- a/virtweb_backend/src/extractors/api_auth_extractor.rs +++ b/virtweb_backend/src/extractors/api_auth_extractor.rs @@ -4,7 +4,7 @@ use crate::api_tokens; use crate::utils::jwt_utils; use crate::utils::time_utils::time; use actix_web::dev::Payload; -use actix_web::error::ErrorBadRequest; +use actix_web::error::{ErrorBadRequest, ErrorUnauthorized}; use actix_web::{Error, FromRequest, HttpRequest}; use std::future::Future; use std::pin::Pin; @@ -108,7 +108,16 @@ impl FromRequest for ApiAuthExtractor { return Err(ErrorBadRequest("JWT method mismatch!")); } - // TODO : check if route is authorized with token + if !token.rights.contains(claims.verb, req.path()) { + log::error!( + "Attempt to use a token for an unauthorized route! (token_id={})", + token.id.0 + ); + return Err(ErrorUnauthorized( + "Token cannot be used to query this route!", + )); + } + // TODO : check for ip restriction // TODO : manually validate all checks @@ -116,7 +125,7 @@ impl FromRequest for ApiAuthExtractor { if token.should_update_last_activity() { if let Err(e) = api_tokens::refresh_last_used(token.id).await { log::error!("Could not update token last activity! {e}"); - return Err(ErrorBadRequest("!")); + return Err(ErrorBadRequest("Couldn't refresh token last activity!")); } } -- 2.45.2 From cbef5f2a78b9d2ee11add6f33c008a5d2ac52acb Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 10 Apr 2024 21:35:53 +0200 Subject: [PATCH 14/36] Enforce IP restriction --- .../src/extractors/api_auth_extractor.rs | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/virtweb_backend/src/extractors/api_auth_extractor.rs b/virtweb_backend/src/extractors/api_auth_extractor.rs index 0f22043..19d5dff 100644 --- a/virtweb_backend/src/extractors/api_auth_extractor.rs +++ b/virtweb_backend/src/extractors/api_auth_extractor.rs @@ -3,6 +3,7 @@ use crate::api_tokens::{Token, TokenID, TokenVerb}; use crate::api_tokens; use crate::utils::jwt_utils; use crate::utils::time_utils::time; +use actix_remote_ip::RemoteIP; use actix_web::dev::Payload; use actix_web::error::{ErrorBadRequest, ErrorUnauthorized}; use actix_web::{Error, FromRequest, HttpRequest}; @@ -28,9 +29,14 @@ impl FromRequest for ApiAuthExtractor { type Error = Error; type Future = Pin>>>; - fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future { + fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { let req = req.clone(); + let remote_ip = match RemoteIP::from_request(&req, payload).into_inner() { + Ok(ip) => ip, + Err(e) => return Box::pin(async { Err(e) }), + }; + Box::pin(async move { let (token_id, token_jwt) = match ( req.headers().get("x-token-id"), @@ -118,9 +124,15 @@ impl FromRequest for ApiAuthExtractor { )); } - // TODO : check for ip restriction - - // TODO : manually validate all checks + if let Some(ip) = token.ip_restriction { + if !ip.contains(remote_ip.0) { + log::error!( + "Attempt to use a token for an unauthorized IP! {remote_ip:?} token_id={}", + token.id.0 + ); + return Err(ErrorUnauthorized("Token cannot be used from this IP!")); + } + } if token.should_update_last_activity() { if let Err(e) = api_tokens::refresh_last_used(token.id).await { -- 2.45.2 From f03b631b6cf4511b85d1f81c5cf51ab5841d9a77 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 17 Apr 2024 21:41:58 +0200 Subject: [PATCH 15/36] Add path globbing --- virtweb_backend/src/api_tokens.rs | 67 +++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/virtweb_backend/src/api_tokens.rs b/virtweb_backend/src/api_tokens.rs index de2591c..0e30eee 100644 --- a/virtweb_backend/src/api_tokens.rs +++ b/virtweb_backend/src/api_tokens.rs @@ -39,10 +39,33 @@ impl TokenRights { } pub fn contains(&self, verb: TokenVerb, path: &str) -> bool { - self.0.contains(&TokenRight { - verb, - path: path.to_string(), - }) + let 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 >= path_split.len() { + continue 'root; + } + + if part != "*" && part != path_split[idx] { + continue 'root; + } + + last_idx = idx; + } + + // Check we visited the whole path + if last_idx + 1 == path_split.len() { + return true; + } + } + + false } } @@ -233,3 +256,39 @@ pub async fn delete(id: TokenID) -> anyhow::Result<()> { } 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/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")); + } +} -- 2.45.2 From 91127ea61fd16fabe494768b67ce6296f86628d9 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 17 Apr 2024 22:20:01 +0200 Subject: [PATCH 16/36] List the tokens from the WebUI --- .../src/controllers/api_tokens_controller.rs | 1 + virtweb_frontend/package-lock.json | 30 ++--- virtweb_frontend/package.json | 1 + virtweb_frontend/src/App.tsx | 3 + virtweb_frontend/src/api/TokensApi.ts | 36 ++++++ .../src/routes/TokensListRoute.tsx | 118 ++++++++++++++++++ .../src/widgets/BaseAuthenticatedPage.tsx | 6 + virtweb_frontend/src/widgets/TimeWidget.tsx | 64 ++++++++++ 8 files changed, 240 insertions(+), 19 deletions(-) create mode 100644 virtweb_frontend/src/api/TokensApi.ts create mode 100644 virtweb_frontend/src/routes/TokensListRoute.tsx create mode 100644 virtweb_frontend/src/widgets/TimeWidget.tsx diff --git a/virtweb_backend/src/controllers/api_tokens_controller.rs b/virtweb_backend/src/controllers/api_tokens_controller.rs index 3f40161..fa32fbd 100644 --- a/virtweb_backend/src/controllers/api_tokens_controller.rs +++ b/virtweb_backend/src/controllers/api_tokens_controller.rs @@ -14,6 +14,7 @@ mod rest_token { #[derive(serde::Serialize)] pub struct RestToken { + #[serde(flatten)] token: Token, } diff --git a/virtweb_frontend/package-lock.json b/virtweb_frontend/package-lock.json index 1ccdb12..09ef043 100644 --- a/virtweb_frontend/package-lock.json +++ b/virtweb_frontend/package-lock.json @@ -27,6 +27,7 @@ "@types/react-syntax-highlighter": "^15.5.11", "@types/uuid": "^9.0.5", "@vitejs/plugin-react": "^4.2.1", + "date-and-time": "^3.1.1", "filesize": "^10.0.12", "humanize-duration": "^3.29.0", "mui-file-input": "^4.0.4", @@ -35,7 +36,7 @@ "react-router-dom": "^6.15.0", "react-syntax-highlighter": "^15.5.0", "react-vnc": "^1.0.0", - "typescript": "^4.1.6", + "typescript": "^5.0.0", "uuid": "^9.0.1", "vite": "^5.0.8", "vite-tsconfig-paths": "^4.2.2", @@ -8877,6 +8878,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-and-time": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-3.1.1.tgz", + "integrity": "sha512-N9kstidT3P0VUk1iKOFilOZ6251r6iTUNx9M9kvgL2jqOk9mljWZUq5CjAtYwCnppWHbERk5YFQUrSbY7FQOpA==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -21890,15 +21896,15 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/unbox-primitive": { @@ -22231,20 +22237,6 @@ } } }, - "node_modules/vite-tsconfig-paths/node_modules/typescript": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.4.tgz", - "integrity": "sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==", - "optional": true, - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", diff --git a/virtweb_frontend/package.json b/virtweb_frontend/package.json index 1c4f96d..55e1ee2 100644 --- a/virtweb_frontend/package.json +++ b/virtweb_frontend/package.json @@ -23,6 +23,7 @@ "@types/react-syntax-highlighter": "^15.5.11", "@types/uuid": "^9.0.5", "@vitejs/plugin-react": "^4.2.1", + "date-and-time": "^3.1.1", "filesize": "^10.0.12", "humanize-duration": "^3.29.0", "mui-file-input": "^4.0.4", diff --git a/virtweb_frontend/src/App.tsx b/virtweb_frontend/src/App.tsx index f4dcc20..14f3f57 100644 --- a/virtweb_frontend/src/App.tsx +++ b/virtweb_frontend/src/App.tsx @@ -32,6 +32,7 @@ import { CreateNWFilterRoute, EditNWFilterRoute, } from "./routes/EditNWFilterRoute"; +import { TokensListRoute } from "./routes/TokensListRoute"; interface AuthContext { signedIn: boolean; @@ -72,6 +73,8 @@ export function App() { } /> } /> + } /> + } /> } /> diff --git a/virtweb_frontend/src/api/TokensApi.ts b/virtweb_frontend/src/api/TokensApi.ts new file mode 100644 index 0000000..d593a31 --- /dev/null +++ b/virtweb_frontend/src/api/TokensApi.ts @@ -0,0 +1,36 @@ +import { APIClient } from "./ApiClient"; + +export interface TokenRight { + verb: "POST" | "GET" | "PUT" | "DELETE" | "PATCH"; + path: string; +} + +export interface APIToken { + id: string; + name: string; + description: string; + created: number; + updated: number; + rights: TokenRight[]; + last_used: number; + ip_restriction?: string; + max_inactivity?: number; +} + +export function APITokenURL(t: APIToken, edit: boolean = false): string { + return `/tokens/${t.id}${edit ? "/edit" : ""}`; +} + +export class TokensApi { + /** + * Get the full list of tokens + */ + static async GetList(): Promise { + return ( + await APIClient.exec({ + method: "GET", + uri: "/tokens/list", + }) + ).data; + } +} diff --git a/virtweb_frontend/src/routes/TokensListRoute.tsx b/virtweb_frontend/src/routes/TokensListRoute.tsx new file mode 100644 index 0000000..14a8d2a --- /dev/null +++ b/virtweb_frontend/src/routes/TokensListRoute.tsx @@ -0,0 +1,118 @@ +import React from "react"; +import { APIToken, APITokenURL, TokensApi } from "../api/TokensApi"; +import { AsyncWidget } from "../widgets/AsyncWidget"; +import { + Button, + IconButton, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, +} from "@mui/material"; +import { RouterLink } from "../widgets/RouterLink"; +import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; +import { useNavigate } from "react-router-dom"; +import VisibilityIcon from "@mui/icons-material/Visibility"; +import { TimeWidget, timeDiff } from "../widgets/TimeWidget"; + +export function TokensListRoute(): React.ReactElement { + const [list, setList] = React.useState(); + + const [count] = React.useState(1); + + const load = async () => { + setList(await TokensApi.GetList()); + }; + + return ( + } + /> + ); +} + +export function TokensListRouteInner(p: { + list: APIToken[]; +}): React.ReactElement { + const navigate = useNavigate(); + + return ( + + + + } + > + + + + + Name + Description + Created + Updated + Last used + IP restriction + Max inactivity + Rights + Actions + + + + {p.list.map((t) => { + return ( + navigate(APITokenURL(t))} + > + {t.name} + {t.description} + + + + + + + + + + {t.ip_restriction} + + {t.max_inactivity && timeDiff(0, t.max_inactivity)} + + + {t.rights.map((r) => { + return ( +
+ {r.verb} {r.path} +
+ ); + })} +
+ + + + + + + + +
+ ); + })} +
+
+
+
+ ); +} diff --git a/virtweb_frontend/src/widgets/BaseAuthenticatedPage.tsx b/virtweb_frontend/src/widgets/BaseAuthenticatedPage.tsx index 4af33e4..7969bbd 100644 --- a/virtweb_frontend/src/widgets/BaseAuthenticatedPage.tsx +++ b/virtweb_frontend/src/widgets/BaseAuthenticatedPage.tsx @@ -1,4 +1,5 @@ import { + mdiApi, mdiBoxShadow, mdiDisc, mdiHome, @@ -72,6 +73,11 @@ export function BaseAuthenticatedPage(): React.ReactElement { uri="/iso" icon={} /> + } + /> ; + return ( + + {timeDiffFromNow(p.time)} + + ); +} -- 2.45.2 From 6ebff3ac0ccb1ba375a3f18a92446075b74d20a0 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 17 Apr 2024 22:22:22 +0200 Subject: [PATCH 17/36] Attempt to fix dependency conflict --- virtweb_frontend/package-lock.json | 24 +++++++++++++++++++----- virtweb_frontend/package.json | 2 +- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/virtweb_frontend/package-lock.json b/virtweb_frontend/package-lock.json index 09ef043..591af9f 100644 --- a/virtweb_frontend/package-lock.json +++ b/virtweb_frontend/package-lock.json @@ -36,7 +36,7 @@ "react-router-dom": "^6.15.0", "react-syntax-highlighter": "^15.5.0", "react-vnc": "^1.0.0", - "typescript": "^5.0.0", + "typescript": "^4.0.0", "uuid": "^9.0.1", "vite": "^5.0.8", "vite-tsconfig-paths": "^4.2.2", @@ -21896,15 +21896,15 @@ } }, "node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=14.17" + "node": ">=4.2.0" } }, "node_modules/unbox-primitive": { @@ -22237,6 +22237,20 @@ } } }, + "node_modules/vite-tsconfig-paths/node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "optional": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", diff --git a/virtweb_frontend/package.json b/virtweb_frontend/package.json index 55e1ee2..63cf182 100644 --- a/virtweb_frontend/package.json +++ b/virtweb_frontend/package.json @@ -32,7 +32,7 @@ "react-router-dom": "^6.15.0", "react-syntax-highlighter": "^15.5.0", "react-vnc": "^1.0.0", - "typescript": "^5.0.0", + "typescript": "^4.0.0", "uuid": "^9.0.1", "vite": "^5.0.8", "vite-tsconfig-paths": "^4.2.2", -- 2.45.2 From 84e293bddb3671280421c5bdf866a0afb3fb6944 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 18 Apr 2024 19:38:25 +0200 Subject: [PATCH 18/36] Start to build routes to create, view and edit API token --- virtweb_frontend/src/App.tsx | 46 ++-- virtweb_frontend/src/api/ServerApi.ts | 2 + virtweb_frontend/src/api/TokensApi.ts | 58 +++++ .../src/routes/EditAPITokenRoute.tsx | 148 ++++++++++++ .../src/routes/NetworksListRoute.tsx | 5 +- .../src/routes/ViewApiTokenRoute.tsx | 53 +++++ .../src/widgets/forms/IPInput.tsx | 12 +- .../src/widgets/tokens/APITokenDetails.tsx | 216 ++++++++++++++++++ 8 files changed, 515 insertions(+), 25 deletions(-) create mode 100644 virtweb_frontend/src/routes/EditAPITokenRoute.tsx create mode 100644 virtweb_frontend/src/routes/ViewApiTokenRoute.tsx create mode 100644 virtweb_frontend/src/widgets/tokens/APITokenDetails.tsx diff --git a/virtweb_frontend/src/App.tsx b/virtweb_frontend/src/App.tsx index 14f3f57..adff3b3 100644 --- a/virtweb_frontend/src/App.tsx +++ b/virtweb_frontend/src/App.tsx @@ -9,30 +9,35 @@ import "./App.css"; import { AuthApi } from "./api/AuthApi"; import { ServerApi } from "./api/ServerApi"; import { - CreateNetworkRoute, - EditNetworkRoute, -} from "./routes/EditNetworkRoute"; -import { CreateVMRoute, EditVMRoute } from "./routes/EditVMRoute"; -import { IsoFilesRoute } from "./routes/IsoFilesRoute"; -import { NetworksListRoute } from "./routes/NetworksListRoute"; -import { NotFoundRoute } from "./routes/NotFound"; -import { SysInfoRoute } from "./routes/SysInfoRoute"; -import { VMListRoute } from "./routes/VMListRoute"; -import { VMRoute } from "./routes/VMRoute"; -import { VNCRoute } from "./routes/VNCRoute"; -import { LoginRoute } from "./routes/auth/LoginRoute"; -import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute"; -import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage"; -import { BaseLoginPage } from "./widgets/BaseLoginPage"; -import { ViewNetworkRoute } from "./routes/ViewNetworkRoute"; -import { HomeRoute } from "./routes/HomeRoute"; -import { NetworkFiltersListRoute } from "./routes/NetworkFiltersListRoute"; -import { ViewNWFilterRoute } from "./routes/ViewNWFilterRoute"; + CreateApiTokenRoute, + EditApiTokenRoute, +} from "./routes/EditAPITokenRoute"; import { CreateNWFilterRoute, EditNWFilterRoute, } from "./routes/EditNWFilterRoute"; +import { + CreateNetworkRoute, + EditNetworkRoute, +} from "./routes/EditNetworkRoute"; +import { CreateVMRoute, EditVMRoute } from "./routes/EditVMRoute"; +import { HomeRoute } from "./routes/HomeRoute"; +import { IsoFilesRoute } from "./routes/IsoFilesRoute"; +import { NetworkFiltersListRoute } from "./routes/NetworkFiltersListRoute"; +import { NetworksListRoute } from "./routes/NetworksListRoute"; +import { NotFoundRoute } from "./routes/NotFound"; +import { SysInfoRoute } from "./routes/SysInfoRoute"; import { TokensListRoute } from "./routes/TokensListRoute"; +import { VMListRoute } from "./routes/VMListRoute"; +import { VMRoute } from "./routes/VMRoute"; +import { VNCRoute } from "./routes/VNCRoute"; +import { ViewApiTokenRoute } from "./routes/ViewApiTokenRoute"; +import { ViewNWFilterRoute } from "./routes/ViewNWFilterRoute"; +import { ViewNetworkRoute } from "./routes/ViewNetworkRoute"; +import { LoginRoute } from "./routes/auth/LoginRoute"; +import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute"; +import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage"; +import { BaseLoginPage } from "./widgets/BaseLoginPage"; interface AuthContext { signedIn: boolean; @@ -74,6 +79,9 @@ export function App() { } /> } /> + } /> + } /> + } /> } /> } /> diff --git a/virtweb_frontend/src/api/ServerApi.ts b/virtweb_frontend/src/api/ServerApi.ts index 99e710c..203d0a0 100644 --- a/virtweb_frontend/src/api/ServerApi.ts +++ b/virtweb_frontend/src/api/ServerApi.ts @@ -27,6 +27,8 @@ export interface ServerConstraints { nwfilter_comment_size: LenConstraint; nwfilter_priority: LenConstraint; nwfilter_selectors_count: LenConstraint; + api_token_name_size: LenConstraint; + api_token_description_size: LenConstraint; } export interface LenConstraint { diff --git a/virtweb_frontend/src/api/TokensApi.ts b/virtweb_frontend/src/api/TokensApi.ts index d593a31..696d4cc 100644 --- a/virtweb_frontend/src/api/TokensApi.ts +++ b/virtweb_frontend/src/api/TokensApi.ts @@ -21,7 +21,30 @@ export function APITokenURL(t: APIToken, edit: boolean = false): string { return `/tokens/${t.id}${edit ? "/edit" : ""}`; } +export interface APITokenPrivateKey { + alg: string; + priv: string; +} + +export interface CreatedAPIToken { + token: APIToken; + priv_key: APITokenPrivateKey; +} + export class TokensApi { + /** + * Create a new API token + */ + static async Create(n: APIToken): Promise { + return ( + await APIClient.exec({ + method: "POST", + uri: "/tokens/create", + jsonData: n, + }) + ).data; + } + /** * Get the full list of tokens */ @@ -33,4 +56,39 @@ export class TokensApi { }) ).data; } + + /** + * Get the information about a single token + */ + static async GetSingle(uuid: string): Promise { + return ( + await APIClient.exec({ + method: "GET", + uri: `/tokens/${uuid}`, + }) + ).data; + } + + /** + * Update an existing API token information + */ + static async Update(n: APIToken): Promise { + return ( + await APIClient.exec({ + method: "PATCH", + uri: `/tokens/${n.id}`, + jsonData: n, + }) + ).data; + } + + /** + * Delete an API token + */ + static async Delete(n: APIToken): Promise { + await APIClient.exec({ + method: "DELETE", + uri: `/tokens/${n.id}`, + }); + } } diff --git a/virtweb_frontend/src/routes/EditAPITokenRoute.tsx b/virtweb_frontend/src/routes/EditAPITokenRoute.tsx new file mode 100644 index 0000000..4f1358d --- /dev/null +++ b/virtweb_frontend/src/routes/EditAPITokenRoute.tsx @@ -0,0 +1,148 @@ +import { Button } from "@mui/material"; +import React from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { APIToken, APITokenURL, TokensApi } from "../api/TokensApi"; +import { useAlert } from "../hooks/providers/AlertDialogProvider"; +import { useLoadingMessage } from "../hooks/providers/LoadingMessageProvider"; +import { useSnackbar } from "../hooks/providers/SnackbarProvider"; +import { time } from "../utils/DateUtils"; +import { AsyncWidget } from "../widgets/AsyncWidget"; +import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; +import { + APITokenDetails, + TokenWidgetStatus, +} from "../widgets/tokens/APITokenDetails"; + +export function CreateApiTokenRoute(): React.ReactElement { + const alert = useAlert(); + const snackbar = useSnackbar(); + const navigate = useNavigate(); + + const [token] = React.useState({ + id: "", + name: "", + description: "", + created: time(), + updated: time(), + last_used: time(), + rights: [], + }); + + const createApiToken = async (n: APIToken) => { + try { + const res = await TokensApi.Create(n); + snackbar("The api token was successfully created!"); + // TODO : show a modal to give token information + navigate(APITokenURL(res.token)); + } catch (e) { + console.error(e); + alert(`Failed to create API token!\n${e}`); + } + }; + + return ( + navigate("/tokens")} + onSave={createApiToken} + /> + ); +} + +export function EditApiTokenRoute(): React.ReactElement { + const alert = useAlert(); + const snackbar = useSnackbar(); + + const { id } = useParams(); + const navigate = useNavigate(); + + const [token, setToken] = React.useState(); + + const load = async () => { + setToken(await TokensApi.GetSingle(id!)); + }; + + const updateApiToken = async (n: APIToken) => { + try { + await TokensApi.Update(n); + snackbar("The token was successfully updated!"); + navigate(APITokenURL(token!)); + } catch (e) { + console.error(e); + alert(`Failed to update token!\n${e}`); + } + }; + + return ( + ( + navigate(`/token/${id}`)} + onSave={updateApiToken} + /> + )} + /> + ); +} + +function EditApiTokenRouteInner(p: { + token: APIToken; + creating: boolean; + onCancel: () => void; + onSave: (token: APIToken) => Promise; +}): React.ReactElement { + const loadingMessage = useLoadingMessage(); + + const [changed, setChanged] = React.useState(false); + + const [, updateState] = React.useState(); + const forceUpdate = React.useCallback(() => updateState({}), []); + + const valueChanged = () => { + setChanged(true); + forceUpdate(); + }; + + const save = async () => { + loadingMessage.show("Saving API token configuration..."); + await p.onSave(p.token); + loadingMessage.hide(); + }; + + return ( + + {changed && ( + + )} + + + } + > + + + ); +} diff --git a/virtweb_frontend/src/routes/NetworksListRoute.tsx b/virtweb_frontend/src/routes/NetworksListRoute.tsx index 8f7c5d3..a8ef9ce 100644 --- a/virtweb_frontend/src/routes/NetworksListRoute.tsx +++ b/virtweb_frontend/src/routes/NetworksListRoute.tsx @@ -1,4 +1,3 @@ -import DeleteIcon from "@mui/icons-material/Delete"; import VisibilityIcon from "@mui/icons-material/Visibility"; import { Button, @@ -13,13 +12,13 @@ import { Typography, } from "@mui/material"; import React from "react"; +import { useNavigate } from "react-router-dom"; import { NetworkApi, NetworkInfo, NetworkURL } from "../api/NetworksApi"; import { AsyncWidget } from "../widgets/AsyncWidget"; import { RouterLink } from "../widgets/RouterLink"; import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; -import { NetworkStatusWidget } from "../widgets/net/NetworkStatusWidget"; -import { useNavigate } from "react-router-dom"; import { NetworkHookStatusWidget } from "../widgets/net/NetworkHookStatusWidget"; +import { NetworkStatusWidget } from "../widgets/net/NetworkStatusWidget"; export function NetworksListRoute(): React.ReactElement { const [list, setList] = React.useState(); diff --git a/virtweb_frontend/src/routes/ViewApiTokenRoute.tsx b/virtweb_frontend/src/routes/ViewApiTokenRoute.tsx new file mode 100644 index 0000000..f94e952 --- /dev/null +++ b/virtweb_frontend/src/routes/ViewApiTokenRoute.tsx @@ -0,0 +1,53 @@ +import { Button } from "@mui/material"; +import React from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { APIToken, APITokenURL, TokensApi } from "../api/TokensApi"; +import { AsyncWidget } from "../widgets/AsyncWidget"; +import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; +import { + APITokenDetails, + TokenWidgetStatus, +} from "../widgets/tokens/APITokenDetails"; + +export function ViewApiTokenRoute() { + const { id } = useParams(); + + const [token, setToken] = React.useState(); + + const load = async () => { + setToken(await TokensApi.GetSingle(id!)); + }; + + return ( + } + /> + ); +} + +function ViewAPITokenRouteInner(p: { token: APIToken }): React.ReactElement { + const navigate = useNavigate(); + + return ( + + + + } + > + + + ); +} diff --git a/virtweb_frontend/src/widgets/forms/IPInput.tsx b/virtweb_frontend/src/widgets/forms/IPInput.tsx index c7db678..90df36a 100644 --- a/virtweb_frontend/src/widgets/forms/IPInput.tsx +++ b/virtweb_frontend/src/widgets/forms/IPInput.tsx @@ -22,14 +22,16 @@ export function IPInput(p: { export function IPInputWithMask(p: { label: string; editable: boolean; + ipAndMask?: string; ip?: string; mask?: number; - onValueChange?: (ip?: string, mask?: number) => void; + onValueChange?: (ip?: string, mask?: number, ipAndMask?: string) => void; version: 4 | 6; }): React.ReactElement { const showSlash = React.useRef(!!p.mask); const currValue = + p.ipAndMask ?? (p.ip ?? "") + (p.mask || showSlash.current ? "/" : "") + (p.mask ?? ""); const { onValueChange, ...props } = p; @@ -38,7 +40,7 @@ export function IPInputWithMask(p: { onValueChange={(v) => { showSlash.current = false; if (!v) { - onValueChange?.(undefined, undefined); + onValueChange?.(undefined, undefined, undefined); return; } @@ -52,7 +54,11 @@ export function IPInputWithMask(p: { mask = sanitizeMask(p.version, split[1]); } - onValueChange?.(ip, mask); + onValueChange?.( + ip, + mask, + mask || showSlash.current ? `${ip}/${mask ?? ""}` : ip + ); }} value={currValue} {...props} diff --git a/virtweb_frontend/src/widgets/tokens/APITokenDetails.tsx b/virtweb_frontend/src/widgets/tokens/APITokenDetails.tsx new file mode 100644 index 0000000..776c969 --- /dev/null +++ b/virtweb_frontend/src/widgets/tokens/APITokenDetails.tsx @@ -0,0 +1,216 @@ +import { Button, Grid } from "@mui/material"; +import React from "react"; +import { useNavigate } from "react-router-dom"; +import { NWFilter, NWFilterApi } from "../../api/NWFilterApi"; +import { NetworkApi, NetworkInfo } from "../../api/NetworksApi"; +import { ServerApi } from "../../api/ServerApi"; +import { APIToken, TokensApi } from "../../api/TokensApi"; +import { VMApi, VMInfo } from "../../api/VMApi"; +import { useAlert } from "../../hooks/providers/AlertDialogProvider"; +import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider"; +import { useSnackbar } from "../../hooks/providers/SnackbarProvider"; +import { AsyncWidget } from "../AsyncWidget"; +import { TabsWidget } from "../TabsWidget"; +import { EditSection } from "../forms/EditSection"; +import { IPInput, IPInputWithMask } from "../forms/IPInput"; +import { ResAutostartInput } from "../forms/ResAutostartInput"; +import { SelectInput } from "../forms/SelectInput"; +import { TextInput } from "../forms/TextInput"; +import { RadioGroupInput } from "../forms/RadioGroupInput"; + +export enum TokenWidgetStatus { + Create, + Read, + Update, +} + +interface DetailsProps { + token: APIToken; + status: TokenWidgetStatus; + onChange?: () => void; +} + +export function APITokenDetails(p: DetailsProps): React.ReactElement { + const [vms, setVMs] = React.useState(); + const [networks, setNetworks] = React.useState(); + const [nwFilters, setNetworkFilters] = React.useState(); + const [tokens, setTokens] = React.useState(); + + const load = async () => { + setVMs(await VMApi.GetList()); + setNetworks(await NetworkApi.GetList()); + setNetworkFilters(await NWFilterApi.GetList()); + setTokens(await TokensApi.GetList()); + }; + + return ( + ( + + )} + /> + ); +} + +enum TokenTab { + General = 0, + Rights, + RawRights, + Danger, +} + +type DetailsInnerProps = DetailsProps & { + vms: VMInfo[]; + networks: NetworkInfo[]; + nwFilters: NWFilter[]; + tokens: APIToken[]; +}; + +function APITokenDetailsInner(p: DetailsInnerProps): React.ReactElement { + const [currTab, setCurrTab] = React.useState(TokenTab.General); + + return ( + <> + + {currTab === TokenTab.General && } + {/* todo: rights */} + {currTab === TokenTab.Danger && } + + ); +} + +function NetworkDetailsTabGeneral(p: DetailsInnerProps): React.ReactElement { + const [ipVersion, setIpVersion] = React.useState<4 | 6>( + (p.token.ip_restriction ?? "").includes(":") ? 6 : 4 + ); + + return ( + + {/* Metadata section */} + + {p.status !== TokenWidgetStatus.Create && ( + + )} + + { + p.token.name = v ?? ""; + p.onChange?.(); + }} + size={ServerApi.Config.constraints.api_token_name_size} + /> + + { + p.token.description = v ?? ""; + p.onChange?.(); + }} + multiline={true} + size={ServerApi.Config.constraints.api_token_description_size} + /> + + + + {(p.status === TokenWidgetStatus.Create || p.token.ip_restriction) && ( + { + setIpVersion(Number(v) as any); + }} + /> + )} + { + p.token.ip_restriction = ipAndMask; + p.onChange?.(); + }} + /> + + {/* TODO : remaining */} + + + ); +} + +function APITokenTabDanger(p: DetailsInnerProps): React.ReactElement { + const confirm = useConfirm(); + const snackbar = useSnackbar(); + const alert = useAlert(); + const navigate = useNavigate(); + + const requestDelete = async () => { + try { + if ( + !(await confirm( + "Do you really want to delete this API token?", + `Delete API token ${p.token.name}`, + "Delete" + )) + ) + return; + + await TokensApi.Delete(p.token); + + navigate("/tokens"); + snackbar("The API token was successfully deleted!"); + } catch (e) { + console.error(e); + alert(`Failed to delete the API token!\n${e}`); + } + }; + + return ( + + ); +} -- 2.45.2 From 3f64cd325921f1b1f5aa80cb20781668d818b182 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 18 Apr 2024 19:39:41 +0200 Subject: [PATCH 19/36] Fix API tokens routes --- virtweb_frontend/src/App.tsx | 6 +++--- virtweb_frontend/src/api/TokensApi.ts | 2 +- virtweb_frontend/src/routes/TokensListRoute.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/virtweb_frontend/src/App.tsx b/virtweb_frontend/src/App.tsx index adff3b3..8e7cd81 100644 --- a/virtweb_frontend/src/App.tsx +++ b/virtweb_frontend/src/App.tsx @@ -79,9 +79,9 @@ export function App() { } /> } /> - } /> - } /> - } /> + } /> + } /> + } /> } /> } /> diff --git a/virtweb_frontend/src/api/TokensApi.ts b/virtweb_frontend/src/api/TokensApi.ts index 696d4cc..8969b47 100644 --- a/virtweb_frontend/src/api/TokensApi.ts +++ b/virtweb_frontend/src/api/TokensApi.ts @@ -18,7 +18,7 @@ export interface APIToken { } export function APITokenURL(t: APIToken, edit: boolean = false): string { - return `/tokens/${t.id}${edit ? "/edit" : ""}`; + return `/token/${t.id}${edit ? "/edit" : ""}`; } export interface APITokenPrivateKey { diff --git a/virtweb_frontend/src/routes/TokensListRoute.tsx b/virtweb_frontend/src/routes/TokensListRoute.tsx index 14a8d2a..fed3e15 100644 --- a/virtweb_frontend/src/routes/TokensListRoute.tsx +++ b/virtweb_frontend/src/routes/TokensListRoute.tsx @@ -47,7 +47,7 @@ export function TokensListRouteInner(p: { + } -- 2.45.2 From 5e134ffba6bb7e21083dcf0d1333b2721e16e044 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 18 Apr 2024 22:44:34 +0200 Subject: [PATCH 20/36] Show private key after API key creation --- .../src/dialogs/CreatedTokenDialog.tsx | 58 +++++++++++++++++++ .../src/routes/EditAPITokenRoute.tsx | 31 +++++++--- virtweb_frontend/src/widgets/InlineCode.tsx | 18 ++++++ .../widgets/net/NetworkHookStatusWidget.tsx | 20 +------ 4 files changed, 99 insertions(+), 28 deletions(-) create mode 100644 virtweb_frontend/src/dialogs/CreatedTokenDialog.tsx create mode 100644 virtweb_frontend/src/widgets/InlineCode.tsx diff --git a/virtweb_frontend/src/dialogs/CreatedTokenDialog.tsx b/virtweb_frontend/src/dialogs/CreatedTokenDialog.tsx new file mode 100644 index 0000000..b654db9 --- /dev/null +++ b/virtweb_frontend/src/dialogs/CreatedTokenDialog.tsx @@ -0,0 +1,58 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Typography, +} from "@mui/material"; +import { useNavigate } from "react-router-dom"; +import { APITokenURL, CreatedAPIToken } from "../api/TokensApi"; +import { CopyToClipboard } from "../widgets/CopyToClipboard"; +import { InlineCode } from "../widgets/InlineCode"; + +export function CreatedTokenDialog(p: { + createdToken: CreatedAPIToken; +}): React.ReactElement { + const navigate = useNavigate(); + + const close = () => { + navigate(APITokenURL(p.createdToken.token)); + }; + return ( + + Token successfully created + + + Your token was successfully created. You need now to copy the private + key, as it will be technically impossible to recover it after closing + this dialog. + + + + + + + + + + + ); +} + +function InfoBlock( + p: React.PropsWithChildren<{ label: string; value: string }> +): React.ReactElement { + return ( +
+ {p.label} + + {p.value} + +
+ ); +} diff --git a/virtweb_frontend/src/routes/EditAPITokenRoute.tsx b/virtweb_frontend/src/routes/EditAPITokenRoute.tsx index 4f1358d..7063028 100644 --- a/virtweb_frontend/src/routes/EditAPITokenRoute.tsx +++ b/virtweb_frontend/src/routes/EditAPITokenRoute.tsx @@ -1,7 +1,13 @@ import { Button } from "@mui/material"; import React from "react"; import { useNavigate, useParams } from "react-router-dom"; -import { APIToken, APITokenURL, TokensApi } from "../api/TokensApi"; +import { + APIToken, + APITokenURL, + CreatedAPIToken, + TokensApi, +} from "../api/TokensApi"; +import { CreatedTokenDialog } from "../dialogs/CreatedTokenDialog"; import { useAlert } from "../hooks/providers/AlertDialogProvider"; import { useLoadingMessage } from "../hooks/providers/LoadingMessageProvider"; import { useSnackbar } from "../hooks/providers/SnackbarProvider"; @@ -18,6 +24,10 @@ export function CreateApiTokenRoute(): React.ReactElement { const snackbar = useSnackbar(); const navigate = useNavigate(); + const [createdToken, setCreatedToken] = React.useState< + CreatedAPIToken | undefined + >(); + const [token] = React.useState({ id: "", name: "", @@ -32,8 +42,7 @@ export function CreateApiTokenRoute(): React.ReactElement { try { const res = await TokensApi.Create(n); snackbar("The api token was successfully created!"); - // TODO : show a modal to give token information - navigate(APITokenURL(res.token)); + setCreatedToken(res); } catch (e) { console.error(e); alert(`Failed to create API token!\n${e}`); @@ -41,12 +50,16 @@ export function CreateApiTokenRoute(): React.ReactElement { }; return ( - navigate("/tokens")} - onSave={createApiToken} - /> + <> + {createdToken && } + + navigate("/tokens")} + onSave={createApiToken} + /> + ); } diff --git a/virtweb_frontend/src/widgets/InlineCode.tsx b/virtweb_frontend/src/widgets/InlineCode.tsx new file mode 100644 index 0000000..38c94c5 --- /dev/null +++ b/virtweb_frontend/src/widgets/InlineCode.tsx @@ -0,0 +1,18 @@ +export function InlineCode(p: React.PropsWithChildren): React.ReactElement { + return ( + + {p.children} + + ); +} diff --git a/virtweb_frontend/src/widgets/net/NetworkHookStatusWidget.tsx b/virtweb_frontend/src/widgets/net/NetworkHookStatusWidget.tsx index 191ca7d..b5d2237 100644 --- a/virtweb_frontend/src/widgets/net/NetworkHookStatusWidget.tsx +++ b/virtweb_frontend/src/widgets/net/NetworkHookStatusWidget.tsx @@ -3,6 +3,7 @@ import React, { PropsWithChildren } from "react"; import { NetworkHookStatus, ServerApi } from "../../api/ServerApi"; import { AsyncWidget } from "../AsyncWidget"; import { CopyToClipboard } from "../CopyToClipboard"; +import { InlineCode } from "../InlineCode"; export function NetworkHookStatusWidget(p: { hiddenIfInstalled: boolean; @@ -72,25 +73,6 @@ function NetworkHookStatusWidgetInner(p: { ); } -function InlineCode(p: PropsWithChildren): React.ReactElement { - return ( - - {p.children} - - ); -} - function CodeBlock(p: PropsWithChildren): React.ReactElement { return (
Date: Thu, 18 Apr 2024 22:45:31 +0200
Subject: [PATCH 21/36] Fix error

---
 virtweb_frontend/src/dialogs/CreatedTokenDialog.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/virtweb_frontend/src/dialogs/CreatedTokenDialog.tsx b/virtweb_frontend/src/dialogs/CreatedTokenDialog.tsx
index b654db9..ee01993 100644
--- a/virtweb_frontend/src/dialogs/CreatedTokenDialog.tsx
+++ b/virtweb_frontend/src/dialogs/CreatedTokenDialog.tsx
@@ -29,7 +29,7 @@ export function CreatedTokenDialog(p: {
           this dialog.
         
 
-        
+        
         
         
       
-- 
2.45.2


From ec25b6e4f196aa37d3a58f2eba2f130bc6cce535 Mon Sep 17 00:00:00 2001
From: Pierre HUBERT 
Date: Fri, 19 Apr 2024 18:52:28 +0200
Subject: [PATCH 22/36] Can configure maximum inactivity of token

---
 virtweb_backend/src/api_tokens.rs             |  6 ++---
 .../src/widgets/tokens/APITokenDetails.tsx    | 27 ++++++++++++++-----
 2 files changed, 24 insertions(+), 9 deletions(-)

diff --git a/virtweb_backend/src/api_tokens.rs b/virtweb_backend/src/api_tokens.rs
index 0e30eee..5cfa856 100644
--- a/virtweb_backend/src/api_tokens.rs
+++ b/virtweb_backend/src/api_tokens.rs
@@ -159,7 +159,7 @@ pub struct NewToken {
     pub description: String,
     pub rights: TokenRights,
     pub ip_restriction: Option,
-    pub delete_after_inactivity: Option,
+    pub max_inactivity: Option,
 }
 
 impl NewToken {
@@ -185,7 +185,7 @@ impl NewToken {
             return Some(err);
         }
 
-        if let Some(t) = self.delete_after_inactivity {
+        if let Some(t) = self.max_inactivity {
             if t < 3600 {
                 return Some("API tokens shall be valid for at least 1 hour!");
             }
@@ -209,7 +209,7 @@ pub async fn create(t: &NewToken) -> anyhow::Result<(Token, TokenPrivKey)> {
         rights: t.rights.clone(),
         last_used: time(),
         ip_restriction: t.ip_restriction,
-        max_inactivity: t.delete_after_inactivity,
+        max_inactivity: t.max_inactivity,
     };
 
     token.save()?;
diff --git a/virtweb_frontend/src/widgets/tokens/APITokenDetails.tsx b/virtweb_frontend/src/widgets/tokens/APITokenDetails.tsx
index 776c969..c725219 100644
--- a/virtweb_frontend/src/widgets/tokens/APITokenDetails.tsx
+++ b/virtweb_frontend/src/widgets/tokens/APITokenDetails.tsx
@@ -12,11 +12,11 @@ import { useSnackbar } from "../../hooks/providers/SnackbarProvider";
 import { AsyncWidget } from "../AsyncWidget";
 import { TabsWidget } from "../TabsWidget";
 import { EditSection } from "../forms/EditSection";
-import { IPInput, IPInputWithMask } from "../forms/IPInput";
-import { ResAutostartInput } from "../forms/ResAutostartInput";
-import { SelectInput } from "../forms/SelectInput";
-import { TextInput } from "../forms/TextInput";
+import { IPInputWithMask } from "../forms/IPInput";
 import { RadioGroupInput } from "../forms/RadioGroupInput";
+import { TextInput } from "../forms/TextInput";
+
+const SECS_PER_DAY = 3600 * 24;
 
 export enum TokenWidgetStatus {
   Create,
@@ -149,7 +149,7 @@ function NetworkDetailsTabGeneral(p: DetailsInnerProps): React.ReactElement {
       
 
       
-        {(p.status === TokenWidgetStatus.Create || p.token.ip_restriction) && (
+        {p.status === TokenWidgetStatus.Create && (
            {
               setIpVersion(Number(v) as any);
             }}
+            label="Token IP restriction version"
           />
         )}
         
 
-        {/* TODO : remaining */}
+         {
+            const secs = Number(v ?? "0") * SECS_PER_DAY;
+            p.token.max_inactivity = secs === 0 ? undefined : secs;
+            p.onChange?.();
+          }}
+        />
       
     
   );
-- 
2.45.2


From c7f72882a7e545600fe3d7f744b907c97f148c02 Mon Sep 17 00:00:00 2001
From: Pierre HUBERT 
Date: Fri, 19 Apr 2024 22:42:01 +0200
Subject: [PATCH 23/36] Add raw API token rights editor

---
 .../src/controllers/server_controller.rs      |   3 +
 virtweb_backend/src/main.rs                   |   2 +-
 virtweb_frontend/src/api/ServerApi.ts         |   1 +
 .../src/widgets/forms/SelectInput.tsx         |   4 +-
 .../src/widgets/tokens/APITokenDetails.tsx    |  14 ++-
 .../src/widgets/tokens/RawRightsEditor.tsx    | 111 ++++++++++++++++++
 6 files changed, 130 insertions(+), 5 deletions(-)
 create mode 100644 virtweb_frontend/src/widgets/tokens/RawRightsEditor.tsx

diff --git a/virtweb_backend/src/controllers/server_controller.rs b/virtweb_backend/src/controllers/server_controller.rs
index 1e729ed..6a80039 100644
--- a/virtweb_backend/src/controllers/server_controller.rs
+++ b/virtweb_backend/src/controllers/server_controller.rs
@@ -53,6 +53,7 @@ struct ServerConstraints {
     nwfilter_selectors_count: LenConstraints,
     api_token_name_size: LenConstraints,
     api_token_description_size: LenConstraints,
+    api_token_right_path_size: LenConstraints,
 }
 
 pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
@@ -110,6 +111,8 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
                 min: constants::API_TOKEN_DESCRIPTION_MIN_LENGTH,
                 max: constants::API_TOKEN_DESCRIPTION_MAX_LENGTH,
             },
+
+            api_token_right_path_size: LenConstraints { min: 0, max: 255 },
         },
     })
 }
diff --git a/virtweb_backend/src/main.rs b/virtweb_backend/src/main.rs
index a968342..5e26b74 100644
--- a/virtweb_backend/src/main.rs
+++ b/virtweb_backend/src/main.rs
@@ -85,7 +85,7 @@ async fn main() -> std::io::Result<()> {
 
         let mut cors = Cors::default()
             .allowed_origin(&AppConfig::get().website_origin)
-            .allowed_methods(vec!["GET", "POST", "DELETE", "PUT"])
+            .allowed_methods(vec!["GET", "POST", "DELETE", "PUT", "PATCH"])
             .allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT])
             .allowed_header(header::CONTENT_TYPE)
             .supports_credentials()
diff --git a/virtweb_frontend/src/api/ServerApi.ts b/virtweb_frontend/src/api/ServerApi.ts
index 203d0a0..845fd77 100644
--- a/virtweb_frontend/src/api/ServerApi.ts
+++ b/virtweb_frontend/src/api/ServerApi.ts
@@ -29,6 +29,7 @@ export interface ServerConstraints {
   nwfilter_selectors_count: LenConstraint;
   api_token_name_size: LenConstraint;
   api_token_description_size: LenConstraint;
+  api_token_right_path_size: LenConstraint;
 }
 
 export interface LenConstraint {
diff --git a/virtweb_frontend/src/widgets/forms/SelectInput.tsx b/virtweb_frontend/src/widgets/forms/SelectInput.tsx
index 792a516..21ac0ae 100644
--- a/virtweb_frontend/src/widgets/forms/SelectInput.tsx
+++ b/virtweb_frontend/src/widgets/forms/SelectInput.tsx
@@ -16,7 +16,7 @@ export interface SelectOption {
 export function SelectInput(p: {
   value?: string;
   editable: boolean;
-  label: string;
+  label?: string;
   options: SelectOption[];
   onValueChange: (o?: string) => void;
 }): React.ReactElement {
@@ -29,7 +29,7 @@ export function SelectInput(p: {
 
   return (
     
-      {p.label}
+      {p.label && {p.label}}