Add API tokens support (#9)
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Make it possible to create token authorized to query predetermined set of routes. Reviewed-on: #9 Co-authored-by: Pierre HUBERT <pierre.git@communiquons.org> Co-committed-by: Pierre HUBERT <pierre.git@communiquons.org>
This commit is contained in:
299
virtweb_backend/src/api_tokens.rs
Normal file
299
virtweb_backend/src/api_tokens.rs
Normal file
@ -0,0 +1,299 @@
|
||||
//! # API tokens management
|
||||
|
||||
use crate::app_config::AppConfig;
|
||||
use crate::constants;
|
||||
use crate::utils::time_utils::time;
|
||||
use actix_http::Method;
|
||||
use basic_jwt::{JWTPrivateKey, JWTPublicKey};
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)]
|
||||
pub struct TokenID(pub uuid::Uuid);
|
||||
|
||||
impl TokenID {
|
||||
/// Parse a string as a token id
|
||||
pub fn parse(t: &str) -> anyhow::Result<Self> {
|
||||
Ok(Self(uuid::Uuid::parse_str(t)?))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||
pub struct TokenRight {
|
||||
verb: TokenVerb,
|
||||
path: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
pub struct TokenRights(Vec<TokenRight>);
|
||||
|
||||
impl TokenRights {
|
||||
pub fn check_error(&self) -> Option<&'static str> {
|
||||
for r in &self.0 {
|
||||
if !r.path.starts_with("/api/") {
|
||||
return Some("All API rights shall start with /api/");
|
||||
}
|
||||
|
||||
if r.path.len() > constants::API_TOKEN_RIGHT_PATH_MAX_LENGTH {
|
||||
return Some("An API path shall not exceed maximum URL size!");
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn contains(&self, verb: TokenVerb, path: &str) -> bool {
|
||||
let req_path_split = path.split('/').collect::<Vec<_>>();
|
||||
|
||||
'root: for r in &self.0 {
|
||||
if r.verb != verb {
|
||||
continue 'root;
|
||||
}
|
||||
|
||||
let mut last_idx = 0;
|
||||
for (idx, part) in r.path.split('/').enumerate() {
|
||||
if idx >= req_path_split.len() {
|
||||
continue 'root;
|
||||
}
|
||||
|
||||
if part != "*" && part != req_path_split[idx] {
|
||||
continue 'root;
|
||||
}
|
||||
|
||||
last_idx = idx;
|
||||
}
|
||||
|
||||
// Check we visited the whole path
|
||||
if last_idx + 1 == req_path_split.len() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
pub struct Token {
|
||||
pub id: TokenID,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
created: u64,
|
||||
updated: u64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pub_key: Option<JWTPublicKey>,
|
||||
pub rights: TokenRights,
|
||||
pub last_used: u64,
|
||||
pub ip_restriction: Option<ipnetwork::IpNetwork>,
|
||||
pub max_inactivity: Option<u64>,
|
||||
}
|
||||
|
||||
impl Token {
|
||||
/// Turn the token into a JSON string
|
||||
fn save(&self) -> anyhow::Result<()> {
|
||||
let json = serde_json::to_string(self)?;
|
||||
|
||||
std::fs::write(AppConfig::get().api_token_definition_path(self.id), json)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load token information from a file
|
||||
fn load_from_file(path: &Path) -> anyhow::Result<Self> {
|
||||
Ok(serde_json::from_str(&std::fs::read_to_string(path)?)?)
|
||||
}
|
||||
|
||||
/// Check whether a token is expired or not
|
||||
pub fn is_expired(&self) -> bool {
|
||||
if let Some(max_inactivity) = self.max_inactivity {
|
||||
if max_inactivity + self.last_used < time() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Check whether last_used shall be updated or not
|
||||
pub fn should_update_last_activity(&self) -> bool {
|
||||
self.last_used + 3600 < time()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum TokenVerb {
|
||||
GET,
|
||||
POST,
|
||||
PUT,
|
||||
PATCH,
|
||||
DELETE,
|
||||
}
|
||||
|
||||
impl TokenVerb {
|
||||
pub fn as_method(&self) -> Method {
|
||||
match self {
|
||||
TokenVerb::GET => Method::GET,
|
||||
TokenVerb::POST => Method::POST,
|
||||
TokenVerb::PUT => Method::PUT,
|
||||
TokenVerb::PATCH => Method::PATCH,
|
||||
TokenVerb::DELETE => Method::DELETE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for TokenVerb {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"GET" => Ok(TokenVerb::GET),
|
||||
"POST" => Ok(TokenVerb::POST),
|
||||
"PUT" => Ok(TokenVerb::PUT),
|
||||
"PATCH" => Ok(TokenVerb::PATCH),
|
||||
"DELETE" => Ok(TokenVerb::DELETE),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Structure used to create a token
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
pub struct NewToken {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub rights: TokenRights,
|
||||
pub ip_restriction: Option<ipnetwork::IpNetwork>,
|
||||
pub max_inactivity: Option<u64>,
|
||||
}
|
||||
|
||||
impl NewToken {
|
||||
/// Check for error in token
|
||||
pub fn check_error(&self) -> Option<&'static str> {
|
||||
if self.name.len() < constants::API_TOKEN_NAME_MIN_LENGTH {
|
||||
return Some("Name is too short!");
|
||||
}
|
||||
|
||||
if self.name.len() > constants::API_TOKEN_NAME_MAX_LENGTH {
|
||||
return Some("Name is too long!");
|
||||
}
|
||||
|
||||
if self.description.len() < constants::API_TOKEN_DESCRIPTION_MIN_LENGTH {
|
||||
return Some("Description is too short!");
|
||||
}
|
||||
|
||||
if self.description.len() > constants::API_TOKEN_DESCRIPTION_MAX_LENGTH {
|
||||
return Some("Description is too long!");
|
||||
}
|
||||
|
||||
if let Some(err) = self.rights.check_error() {
|
||||
return Some(err);
|
||||
}
|
||||
|
||||
if let Some(t) = self.max_inactivity {
|
||||
if t < 3600 {
|
||||
return Some("API tokens shall be valid for at least 1 hour!");
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new Token
|
||||
pub async fn create(t: &NewToken) -> anyhow::Result<(Token, JWTPrivateKey)> {
|
||||
let priv_key = JWTPrivateKey::generate_ec384_signing_key()?;
|
||||
let pub_key = priv_key.to_public_key()?;
|
||||
|
||||
let token = Token {
|
||||
name: t.name.to_string(),
|
||||
description: t.description.to_string(),
|
||||
id: TokenID(uuid::Uuid::new_v4()),
|
||||
created: time(),
|
||||
updated: time(),
|
||||
pub_key: Some(pub_key),
|
||||
rights: t.rights.clone(),
|
||||
last_used: time(),
|
||||
ip_restriction: t.ip_restriction,
|
||||
max_inactivity: t.max_inactivity,
|
||||
};
|
||||
|
||||
token.save()?;
|
||||
|
||||
Ok((token, priv_key))
|
||||
}
|
||||
|
||||
/// Get the entire list of api tokens
|
||||
pub async fn full_list() -> anyhow::Result<Vec<Token>> {
|
||||
let mut list = Vec::new();
|
||||
for f in std::fs::read_dir(AppConfig::get().api_tokens_path())? {
|
||||
list.push(Token::load_from_file(&f?.path())?);
|
||||
}
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
/// Get the information about a single token
|
||||
pub async fn get_single(id: TokenID) -> anyhow::Result<Token> {
|
||||
Token::load_from_file(&AppConfig::get().api_token_definition_path(id))
|
||||
}
|
||||
|
||||
/// Update API tokens rights
|
||||
pub async fn update_rights(id: TokenID, rights: TokenRights) -> anyhow::Result<()> {
|
||||
let mut token = get_single(id).await?;
|
||||
token.rights = rights;
|
||||
token.updated = time();
|
||||
token.save()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set last_used value of token
|
||||
pub async fn refresh_last_used(id: TokenID) -> anyhow::Result<()> {
|
||||
let mut token = get_single(id).await?;
|
||||
token.last_used = time();
|
||||
token.save()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete an API token
|
||||
pub async fn delete(id: TokenID) -> anyhow::Result<()> {
|
||||
let path = AppConfig::get().api_token_definition_path(id);
|
||||
if path.exists() {
|
||||
std::fs::remove_file(path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::api_tokens::{TokenRight, TokenRights, TokenVerb};
|
||||
|
||||
#[test]
|
||||
fn test_rights_patch() {
|
||||
let rights = TokenRights(vec![
|
||||
TokenRight {
|
||||
path: "/api/vm/*".to_string(),
|
||||
verb: TokenVerb::GET,
|
||||
},
|
||||
TokenRight {
|
||||
path: "/api/vm/a".to_string(),
|
||||
verb: TokenVerb::PUT,
|
||||
},
|
||||
TokenRight {
|
||||
path: "/api/vm/a/other".to_string(),
|
||||
verb: TokenVerb::DELETE,
|
||||
},
|
||||
TokenRight {
|
||||
path: "/api/net/create".to_string(),
|
||||
verb: TokenVerb::POST,
|
||||
},
|
||||
]);
|
||||
|
||||
assert!(rights.contains(TokenVerb::GET, "/api/vm/ab"));
|
||||
assert!(!rights.contains(TokenVerb::GET, "/api/vm"));
|
||||
assert!(!rights.contains(TokenVerb::GET, "/api/vm/ab/c"));
|
||||
assert!(rights.contains(TokenVerb::PUT, "/api/vm/a"));
|
||||
assert!(!rights.contains(TokenVerb::PUT, "/api/vm/other"));
|
||||
assert!(rights.contains(TokenVerb::POST, "/api/net/create"));
|
||||
assert!(!rights.contains(TokenVerb::GET, "/api/net/create"));
|
||||
assert!(!rights.contains(TokenVerb::POST, "/api/net/b"));
|
||||
assert!(!rights.contains(TokenVerb::POST, "/api/net/create/b"));
|
||||
}
|
||||
}
|
@ -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;
|
||||
@ -268,6 +269,14 @@ 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)
|
||||
}
|
||||
|
||||
pub fn api_token_definition_path(&self, id: TokenID) -> PathBuf {
|
||||
self.api_tokens_path().join(format!("{}.json", id.0))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
|
@ -89,3 +89,21 @@ 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;
|
||||
|
||||
/// API token right path max length
|
||||
pub const API_TOKEN_RIGHT_PATH_MAX_LENGTH: usize = 255;
|
||||
|
100
virtweb_backend/src/controllers/api_tokens_controller.rs
Normal file
100
virtweb_backend/src/controllers/api_tokens_controller.rs
Normal 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 actix_web::{web, HttpResponse};
|
||||
use basic_jwt::JWTPrivateKey;
|
||||
|
||||
/// Create a special module for REST token to enforce usage of constructor function
|
||||
mod rest_token {
|
||||
use crate::api_tokens::Token;
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct RestToken {
|
||||
#[serde(flatten)]
|
||||
token: Token,
|
||||
}
|
||||
|
||||
impl RestToken {
|
||||
pub fn new(mut token: Token) -> Self {
|
||||
token.pub_key = None;
|
||||
Self { token }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct CreateTokenResult {
|
||||
token: RestToken,
|
||||
priv_key: JWTPrivateKey,
|
||||
}
|
||||
|
||||
/// 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())
|
||||
}
|
@ -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;
|
||||
|
@ -51,6 +51,9 @@ struct ServerConstraints {
|
||||
nwfilter_comment_size: LenConstraints,
|
||||
nwfilter_priority: SLenConstraints,
|
||||
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 {
|
||||
@ -98,6 +101,21 @@ 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,
|
||||
},
|
||||
|
||||
api_token_right_path_size: LenConstraints {
|
||||
min: 0,
|
||||
max: constants::API_TOKEN_RIGHT_PATH_MAX_LENGTH,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
151
virtweb_backend/src/extractors/api_auth_extractor.rs
Normal file
151
virtweb_backend/src/extractors/api_auth_extractor.rs
Normal file
@ -0,0 +1,151 @@
|
||||
use crate::api_tokens::{Token, TokenID, TokenVerb};
|
||||
|
||||
use crate::api_tokens;
|
||||
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};
|
||||
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();
|
||||
|
||||
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"),
|
||||
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 token
|
||||
.pub_key
|
||||
.as_ref()
|
||||
.expect("All tokens shall have public key!")
|
||||
.validate_jwt::<TokenClaims>(&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!"));
|
||||
}
|
||||
|
||||
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!",
|
||||
));
|
||||
}
|
||||
|
||||
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 {
|
||||
log::error!("Could not update token last activity! {e}");
|
||||
return Err(ErrorBadRequest("Couldn't refresh token last activity!"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ApiAuthExtractor { token, claims })
|
||||
})
|
||||
}
|
||||
}
|
@ -1,2 +1,3 @@
|
||||
pub mod api_auth_extractor;
|
||||
pub mod auth_extractor;
|
||||
pub mod local_auth_extractor;
|
||||
|
@ -1,4 +1,5 @@
|
||||
pub mod actors;
|
||||
pub mod api_tokens;
|
||||
pub mod app_config;
|
||||
pub mod constants;
|
||||
pub mod controllers;
|
||||
|
@ -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()
|
||||
@ -84,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()
|
||||
@ -276,6 +277,27 @@ async fn main() -> std::io::Result<()> {
|
||||
"/api/nwfilter/{uid}",
|
||||
web::delete().to(nwfilter_controller::delete),
|
||||
)
|
||||
// API tokens controller
|
||||
.route(
|
||||
"/api/token/create",
|
||||
web::post().to(api_tokens_controller::create),
|
||||
)
|
||||
.route(
|
||||
"/api/token/list",
|
||||
web::get().to(api_tokens_controller::list),
|
||||
)
|
||||
.route(
|
||||
"/api/token/{uid}",
|
||||
web::get().to(api_tokens_controller::get_single),
|
||||
)
|
||||
.route(
|
||||
"/api/token/{uid}",
|
||||
web::patch().to(api_tokens_controller::update),
|
||||
)
|
||||
.route(
|
||||
"/api/token/{uid}",
|
||||
web::delete().to(api_tokens_controller::delete),
|
||||
)
|
||||
// Static assets
|
||||
.route("/", web::get().to(static_controller::root_index))
|
||||
.route(
|
||||
|
@ -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/")
|
||||
{
|
||||
|
Reference in New Issue
Block a user