13 Commits

Author SHA1 Message Date
b1937d42a2 Create a basic API client
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-04-10 21:03:05 +02:00
23fc10bb60 Merge branch 'master' into api
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-04-10 16:47:31 +00:00
631cc96537 Give more appropriate name to define authorized route for API token
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is passing
2024-04-09 21:53:11 +02:00
fd3df3d214 Start to implement API tokens checks
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-04-09 21:49:26 +02:00
418055a640 Forgot to save new tokens...
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-09 19:41:33 +02:00
0ee2c41f3c Can update and delete API tokens
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-04-09 19:39:41 +02:00
e938b5a423 Merge branch 'master' into api
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-04-09 17:06:18 +00:00
0c5a232a25 Can get a single API token
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-04-09 19:04:49 +02:00
f56e9c14b2 Can get the list of tokens 2024-04-09 18:56:12 +02:00
60a3cb3d10 Can create API tokens 2024-04-09 18:36:18 +02:00
bab34b7c7f Add more test to validate JWTs
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2024-04-09 18:04:00 +02:00
0217d1c53d WIP
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-04-08 22:46:17 +02:00
ab7907d947 WIP 2024-04-08 22:19:28 +02:00
16 changed files with 997 additions and 4 deletions

View File

@@ -568,6 +568,12 @@ dependencies = [
"rustc-demangle", "rustc-demangle",
] ]
[[package]]
name = "base16ct"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.20.0" version = "0.20.0"
@@ -586,6 +592,12 @@ version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51"
[[package]]
name = "base64ct"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]] [[package]]
name = "bincode" name = "bincode"
version = "2.0.0-rc.3" version = "2.0.0-rc.3"
@@ -792,6 +804,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]] [[package]]
name = "convert_case" name = "convert_case"
version = "0.4.0" version = "0.4.0"
@@ -890,6 +908,18 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" 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]] [[package]]
name = "crypto-common" name = "crypto-common"
version = "0.1.6" version = "0.1.6"
@@ -945,6 +975,17 @@ dependencies = [
"syn 2.0.58", "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]] [[package]]
name = "deranged" name = "deranged"
version = "0.3.11" version = "0.3.11"
@@ -974,6 +1015,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [ dependencies = [
"block-buffer", "block-buffer",
"const-oid",
"crypto-common", "crypto-common",
"subtle", "subtle",
] ]
@@ -984,12 +1026,47 @@ version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" 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]] [[package]]
name = "either" name = "either"
version = "1.10.0" version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" 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]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.33" version = "0.8.33"
@@ -1069,6 +1146,16 @@ dependencies = [
"simd-adler32", "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]] [[package]]
name = "flate2" name = "flate2"
version = "1.0.28" version = "1.0.28"
@@ -1215,6 +1302,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [ dependencies = [
"typenum", "typenum",
"version_check", "version_check",
"zeroize",
] ]
[[package]] [[package]]
@@ -1224,8 +1312,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"wasi", "wasi",
"wasm-bindgen",
] ]
[[package]] [[package]]
@@ -1254,6 +1344,17 @@ version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" 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]] [[package]]
name = "h2" name = "h2"
version = "0.3.26" version = "0.3.26"
@@ -1596,6 +1697,21 @@ dependencies = [
"wasm-bindgen", "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]] [[package]]
name = "language-tags" name = "language-tags"
version = "0.3.2" version = "0.3.2"
@@ -2007,6 +2123,18 @@ dependencies = [
"vcpkg", "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]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.1" version = "0.12.1"
@@ -2042,6 +2170,25 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" 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 = "pem-rfc7468"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
dependencies = [
"base64ct",
]
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.1" version = "2.3.1"
@@ -2080,6 +2227,16 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 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]] [[package]]
name = "pkg-config" name = "pkg-config"
version = "0.3.30" version = "0.3.30"
@@ -2123,6 +2280,15 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 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]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.79" version = "1.0.79"
@@ -2367,6 +2533,16 @@ dependencies = [
"winreg", "winreg",
] ]
[[package]]
name = "rfc6979"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
dependencies = [
"hmac",
"subtle",
]
[[package]] [[package]]
name = "rgb" name = "rgb"
version = "0.8.37" version = "0.8.37"
@@ -2376,6 +2552,21 @@ dependencies = [
"bytemuck", "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]] [[package]]
name = "rust-embed" name = "rust-embed"
version = "8.3.0" version = "8.3.0"
@@ -2484,6 +2675,20 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 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]] [[package]]
name = "security-framework" name = "security-framework"
version = "2.10.0" version = "2.10.0"
@@ -2605,6 +2810,16 @@ dependencies = [
"libc", "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]] [[package]]
name = "simd-adler32" name = "simd-adler32"
version = "0.3.7" version = "0.3.7"
@@ -2620,6 +2835,18 @@ dependencies = [
"quote", "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]] [[package]]
name = "slab" name = "slab"
version = "0.4.9" version = "0.4.9"
@@ -2654,6 +2881,16 @@ dependencies = [
"lock_api", "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]] [[package]]
name = "strsim" name = "strsim"
version = "0.10.0" version = "0.10.0"
@@ -3020,6 +3257,12 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.0" version = "2.5.0"
@@ -3132,11 +3375,13 @@ dependencies = [
"bytes", "bytes",
"clap", "clap",
"dotenvy", "dotenvy",
"elliptic-curve",
"env_logger", "env_logger",
"futures", "futures",
"futures-util", "futures-util",
"image", "image",
"ipnetwork", "ipnetwork",
"jsonwebtoken",
"lazy-regex", "lazy-regex",
"lazy_static", "lazy_static",
"light-openid", "light-openid",
@@ -3144,6 +3389,7 @@ dependencies = [
"mime_guess", "mime_guess",
"nix", "nix",
"num", "num",
"p384",
"quick-xml", "quick-xml",
"rand", "rand",
"reqwest", "reqwest",
@@ -3500,6 +3746,12 @@ dependencies = [
"syn 2.0.58", "syn 2.0.58",
] ]
[[package]]
name = "zeroize"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
[[package]] [[package]]
name = "zstd" name = "zstd"
version = "0.13.1" version = "0.13.1"

View File

@@ -45,3 +45,6 @@ rust-embed = { version = "8.3.0" }
mime_guess = "2.0.4" mime_guess = "2.0.4"
dotenvy = "0.15.7" dotenvy = "0.15.7"
nix = { version = "0.28.0", features = ["net"] } nix = { version = "0.28.0", features = ["net"] }
jsonwebtoken = "9.3.0"
elliptic-curve = { version = "0.13.8", features = ["pkcs8","pem" ] }
p384 = { version = "0.13.0", features = ["ecdsa", "pkcs8", "pem"] }

View File

@@ -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<String>,
}
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!")
}

View File

@@ -0,0 +1,220 @@
//! # API tokens management
use crate::app_config::AppConfig;
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;
use std::str::FromStr;
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)]
pub struct TokenID(pub uuid::Uuid);
impl TokenID {
/// Parse a string as a token id
pub fn parse(t: &str) -> anyhow::Result<Self> {
Ok(Self(uuid::Uuid::parse_str(t)?))
}
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct TokenRight {
verb: TokenVerb,
path: String,
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct TokenRights(Vec<TokenRight>);
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct Token {
pub id: TokenID,
pub name: String,
pub description: String,
created: u64,
updated: u64,
#[serde(skip_serializing_if = "TokenPubKey::is_invalid")]
pub pub_key: TokenPubKey,
pub rights: TokenRights,
pub last_used: u64,
pub ip_restriction: Option<ipnetwork::IpNetwork>,
pub max_inactivity: Option<u64>,
}
impl Token {
/// Turn the token into a JSON string
fn save(&self) -> anyhow::Result<()> {
let json = serde_json::to_string(self)?;
std::fs::write(AppConfig::get().api_token_definition_path(self.id), json)?;
Ok(())
}
/// Load token information from a file
fn load_from_file(path: &Path) -> anyhow::Result<Self> {
Ok(serde_json::from_str(&std::fs::read_to_string(path)?)?)
}
/// Check whether a token is expired or not
pub fn is_expired(&self) -> bool {
if let Some(max_inactivity) = self.max_inactivity {
if max_inactivity + self.last_used < time() {
return true;
}
}
false
}
/// Check whether last_used shall be updated or not
pub fn should_update_last_activity(&self) -> bool {
self.last_used + 3600 < time()
}
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, Eq, PartialEq)]
pub enum TokenVerb {
GET,
POST,
PUT,
PATCH,
DELETE,
}
impl TokenVerb {
pub fn as_method(&self) -> Method {
match self {
TokenVerb::GET => Method::GET,
TokenVerb::POST => Method::POST,
TokenVerb::PUT => Method::PUT,
TokenVerb::PATCH => Method::PATCH,
TokenVerb::DELETE => Method::DELETE,
}
}
}
impl FromStr for TokenVerb {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"GET" => Ok(TokenVerb::GET),
"POST" => Ok(TokenVerb::POST),
"PUT" => Ok(TokenVerb::PUT),
"PATCH" => Ok(TokenVerb::PATCH),
"DELETE" => Ok(TokenVerb::DELETE),
_ => Err(()),
}
}
}
/// Structure used to create a token
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct NewToken {
pub name: String,
pub description: String,
pub rights: TokenRights,
pub ip_restriction: Option<ipnetwork::IpNetwork>,
pub delete_after_inactivity: Option<u64>,
}
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> {
if self.name.len() < constants::API_TOKEN_NAME_MIN_LENGTH {
return Some("Name is too short!");
}
if self.name.len() > constants::API_TOKEN_NAME_MAX_LENGTH {
return Some("Name is too long!");
}
if self.description.len() < constants::API_TOKEN_DESCRIPTION_MIN_LENGTH {
return Some("Description is too short!");
}
if self.description.len() > constants::API_TOKEN_DESCRIPTION_MAX_LENGTH {
return Some("Description is too long!");
}
if let Some(err) = self.rights.check_error() {
return Some(err);
}
if let Some(t) = self.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(t: &NewToken) -> anyhow::Result<(Token, TokenPrivKey)> {
let (pub_key, priv_key) = jwt_utils::generate_key_pair()?;
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: t.rights.clone(),
last_used: time(),
ip_restriction: t.ip_restriction,
max_inactivity: t.delete_after_inactivity,
};
token.save()?;
Ok((token, priv_key))
}
/// Get the entire list of api tokens
pub async fn full_list() -> anyhow::Result<Vec<Token>> {
let mut list = Vec::new();
for f in std::fs::read_dir(AppConfig::get().api_tokens_path())? {
list.push(Token::load_from_file(&f?.path())?);
}
Ok(list)
}
/// Get the information about a single token
pub async fn get_single(id: TokenID) -> anyhow::Result<Token> {
Token::load_from_file(&AppConfig::get().api_token_definition_path(id))
}
/// Update API tokens rights
pub async fn update_rights(id: TokenID, rights: TokenRights) -> anyhow::Result<()> {
let mut token = get_single(id).await?;
token.rights = rights;
token.updated = time();
token.save()?;
Ok(())
}
/// 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(())
}

View File

@@ -1,3 +1,4 @@
use crate::api_tokens::TokenID;
use crate::constants; use crate::constants;
use crate::libvirt_lib_structures::XMLUuid; use crate::libvirt_lib_structures::XMLUuid;
use crate::libvirt_rest_structures::net::NetworkName; use crate::libvirt_rest_structures::net::NetworkName;
@@ -268,6 +269,14 @@ impl AppConfig {
self.definitions_path() self.definitions_path()
.join(format!("nwfilter-{}.json", name.0)) .join(format!("nwfilter-{}.json", name.0))
} }
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)] #[derive(Debug, Clone, serde::Serialize)]

View File

@@ -89,3 +89,18 @@ pub const NAT_MODE_ENV_VAR_NAME: &str = "NAT_MODE";
/// Nat hook file path /// Nat hook file path
pub const NAT_HOOK_PATH: &str = "/etc/libvirt/hooks/network"; 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;

View File

@@ -0,0 +1,100 @@
//! # API tokens management
use crate::api_tokens;
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;
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<NewToken>) -> 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,
}))
}
/// Get the list of API tokens
pub async fn list() -> HttpResult {
let list = api_tokens::full_list()
.await?
.into_iter()
.map(RestToken::new)
.collect::<Vec<_>>();
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<TokenIDInPath>) -> HttpResult {
let token = api_tokens::get_single(path.uid).await?;
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<TokenIDInPath>,
body: web::Json<UpdateTokenBody>,
) -> 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<TokenIDInPath>) -> HttpResult {
api_tokens::delete(path.uid).await?;
Ok(HttpResponse::Accepted().finish())
}

View File

@@ -6,6 +6,7 @@ use std::error::Error;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::io::ErrorKind; use std::io::ErrorKind;
pub mod api_tokens_controller;
pub mod auth_controller; pub mod auth_controller;
pub mod iso_controller; pub mod iso_controller;
pub mod network_controller; pub mod network_controller;

View File

@@ -51,6 +51,8 @@ struct ServerConstraints {
nwfilter_comment_size: LenConstraints, nwfilter_comment_size: LenConstraints,
nwfilter_priority: SLenConstraints, nwfilter_priority: SLenConstraints,
nwfilter_selectors_count: LenConstraints, nwfilter_selectors_count: LenConstraints,
api_token_name_size: LenConstraints,
api_token_description_size: LenConstraints,
} }
pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder { 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, max: 1000,
}, },
nwfilter_selectors_count: LenConstraints { min: 0, max: 1 }, 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,
},
}, },
}) })
} }

View File

@@ -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<Box<dyn Future<Output = Result<Self, Self::Error>>>>;
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::<TokenClaims>(&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 })
})
}
}

View File

@@ -1,2 +1,3 @@
pub mod api_auth_extractor;
pub mod auth_extractor; pub mod auth_extractor;
pub mod local_auth_extractor; pub mod local_auth_extractor;

View File

@@ -1,4 +1,5 @@
pub mod actors; pub mod actors;
pub mod api_tokens;
pub mod app_config; pub mod app_config;
pub mod constants; pub mod constants;
pub mod controllers; pub mod controllers;

View File

@@ -22,8 +22,8 @@ use virtweb_backend::constants::{
MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION, SESSION_COOKIE_NAME, MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION, SESSION_COOKIE_NAME,
}; };
use virtweb_backend::controllers::{ use virtweb_backend::controllers::{
auth_controller, iso_controller, network_controller, nwfilter_controller, server_controller, api_tokens_controller, auth_controller, iso_controller, network_controller,
static_controller, vm_controller, nwfilter_controller, server_controller, static_controller, vm_controller,
}; };
use virtweb_backend::libvirt_client::LibVirtClient; use virtweb_backend::libvirt_client::LibVirtClient;
use virtweb_backend::middlewares::auth_middleware::AuthChecker; 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().disks_storage_path()).unwrap();
files_utils::create_directory_if_missing(AppConfig::get().nat_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().definitions_path()).unwrap();
files_utils::create_directory_if_missing(AppConfig::get().api_tokens_path()).unwrap();
let conn = Data::new(LibVirtClient( let conn = Data::new(LibVirtClient(
LibVirtActor::connect() LibVirtActor::connect()
@@ -276,6 +277,27 @@ async fn main() -> std::io::Result<()> {
"/api/nwfilter/{uid}", "/api/nwfilter/{uid}",
web::delete().to(nwfilter_controller::delete), web::delete().to(nwfilter_controller::delete),
) )
// API tokens controller
.route(
"/api/tokens/create",
web::post().to(api_tokens_controller::create),
)
.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::patch().to(api_tokens_controller::update),
)
.route(
"/api/tokens/{uid}",
web::delete().to(api_tokens_controller::delete),
)
// Static assets // Static assets
.route("/", web::get().to(static_controller::root_index)) .route("/", web::get().to(static_controller::root_index))
.route( .route(

View File

@@ -3,6 +3,7 @@ use std::rc::Rc;
use crate::app_config::AppConfig; use crate::app_config::AppConfig;
use crate::constants; use crate::constants;
use crate::extractors::api_auth_extractor::ApiAuthExtractor;
use crate::extractors::auth_extractor::AuthExtractor; use crate::extractors::auth_extractor::AuthExtractor;
use actix_web::body::EitherBody; use actix_web::body::EitherBody;
use actix_web::dev::Payload; use actix_web::dev::Payload;
@@ -68,8 +69,28 @@ where
let auth_disabled = AppConfig::get().unsecure_disable_auth; let auth_disabled = AppConfig::get().unsecure_disable_auth;
// Check authentication, if required // Check API authentication
if !auth_disabled 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()) && !constants::ROUTES_WITHOUT_AUTH.contains(&req.path())
&& req.path().starts_with("/api/") && req.path().starts_with("/api/")
{ {

View File

@@ -0,0 +1,146 @@
use elliptic_curve::pkcs8::EncodePublicKey;
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Validation};
use p384::ecdsa::{SigningKey, VerifyingKey};
use p384::pkcs8::{EncodePrivateKey, LineEnding};
use rand::rngs::OsRng;
use serde::de::DeserializeOwned;
use serde::Serialize;
#[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 used to validate JWT.
///
/// It is a hack to hide public key when getting the list of tokens
None,
/// ECDSA with SHA2-384 variant
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 {
ES384 { r#priv: String },
}
/// Generate a new token keypair
pub fn generate_key_pair() -> anyhow::Result<(TokenPubKey, TokenPrivKey)> {
let signing_key = SigningKey::random(&mut OsRng);
let priv_pem = signing_key
.to_pkcs8_der()?
.to_pem("PRIVATE KEY", LineEnding::LF)?
.to_string();
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 },
TokenPrivKey::ES384 { r#priv: priv_pem },
))
}
/// Sign JWT with a private key
pub fn sign_jwt<C: Serialize>(key: &TokenPrivKey, claims: &C) -> anyhow::Result<String> {
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<E: DeserializeOwned>(key: &TokenPubKey, token: &str) -> anyhow::Result<E> {
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::<E>(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,
}
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::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)
}
#[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::<Claims>(&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::<Claims>(&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::<Claims>(&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::<Claims>(&pub_key, &format!("{jwt}bad"))
.expect_err("JWT should not have validated!");
}
}

View File

@@ -1,5 +1,6 @@
pub mod disks_utils; pub mod disks_utils;
pub mod files_utils; pub mod files_utils;
pub mod jwt_utils;
pub mod net_utils; pub mod net_utils;
pub mod rand_utils; pub mod rand_utils;
pub mod time_utils; pub mod time_utils;