use crate::component::auth::jwt::UserUuid; use crate::api::workspace::{COLLAB_OBJECT_ID_PATH, WORKSPACE_ID_PATH}; use actix_router::{Path, Url}; use actix_service::{forward_ready, Service, Transform}; use actix_web::dev::{ResourceDef, ServiceRequest, ServiceResponse}; use actix_web::http::Method; use actix_web::Error; use async_trait::async_trait; use futures_util::future::LocalBoxFuture; use actix_web::web::Data; use dashmap::DashMap; use once_cell::sync::Lazy; use std::collections::HashMap; use std::future::{ready, Ready}; use std::sync::Arc; use tracing::error; use crate::state::AppState; use app_error::AppError; use uuid::Uuid; static RESOURCE_DEF_CACHE: Lazy> = Lazy::new(DashMap::new); #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum AccessResource { Workspace, Collab, } /// The access control service for http request. /// It is used to check the permission of the request if the request is related to workspace or collab. /// If the request is not related to workspace or collab, it will be skipped. /// /// The collab and workspace access control can be separated into different traits. Currently, they are /// combined into one trait. #[async_trait] pub trait MiddlewareAccessControl: Send + Sync { fn resource(&self) -> AccessResource; #[allow(unused_variables)] async fn check_resource_permission( &self, uid: &i64, resource_id: &str, method: Method, path: &Path, ) -> Result<(), AppError>; } /// Implement the access control for the workspace and collab. /// It will check the permission of the request if the request is related to workspace or collab. #[derive(Clone, Default)] pub struct MiddlewareAccessControlTransform { controllers: Arc>>, } impl MiddlewareAccessControlTransform { pub fn new() -> Self { Self::default() } pub fn with_acs( mut self, access_control_service: T, ) -> Self { let resource = access_control_service.resource(); Arc::make_mut(&mut self.controllers).insert(resource, Arc::new(access_control_service)); self } } impl Transform for MiddlewareAccessControlTransform where S: Service, Error = Error>, S::Future: 'static, B: 'static, { type Response = ServiceResponse; type Error = Error; type Transform = AccessControlMiddleware; type InitError = (); type Future = Ready>; fn new_transform(&self, service: S) -> Self::Future { ready(Ok(AccessControlMiddleware { service, controllers: self.controllers.clone(), })) } } /// Each request will be handled by this middleware. It will check the permission of the request /// if the request is related to workspace or collab. The [WORKSPACE_ID_PATH] and [COLLAB_OBJECT_ID_PATH] /// are used to identify the workspace and collab. /// /// For example, if the request path is `/api/workspace/{workspace_id}/collab/{object_id}`, then the /// [AccessControlMiddleware] will check the permission of the workspace and collab. /// /// pub struct AccessControlMiddleware { service: S, controllers: Arc>>, } impl Service for AccessControlMiddleware where S: Service, Error = Error>, S::Future: 'static, B: 'static, { type Response = ServiceResponse; type Error = Error; type Future = LocalBoxFuture<'static, Result>; forward_ready!(service); fn call(&self, mut req: ServiceRequest) -> Self::Future { let path = req.match_pattern().map(|pattern| { // Create ResourceDef will cause memory leak, so we use the cache to store the ResourceDef let mut path = req.match_info().clone(); RESOURCE_DEF_CACHE .entry(pattern.to_owned()) .or_insert_with(|| ResourceDef::new(pattern)) .value() .capture_match_info(&mut path); path }); match path { None => { let fut = self.service.call(req); Box::pin(fut) }, Some(path) => { let user_uuid = req.extract::(); let user_cache = req .app_data::>() .map(|state| state.user_cache.clone()); let uid = async { let user_uuid = user_uuid.await.map_err(|err| { AppError::Internal(anyhow::anyhow!( "Can't find the user uuid from the request: {}", err )) })?; user_cache .ok_or_else(|| { AppError::Internal(anyhow::anyhow!("AppState is not found in the request")) })? .get_user_uid(&user_uuid) .await }; let workspace_id = path .get(WORKSPACE_ID_PATH) .and_then(|id| Uuid::parse_str(id).ok()); let object_id = path.get(COLLAB_OBJECT_ID_PATH).map(|id| id.to_string()); let method = req.method().clone(); let fut = self.service.call(req); let services = self.controllers.clone(); Box::pin(async move { // If the workspace_id or collab_object_id is not present, skip the access control if workspace_id.is_none() && object_id.is_none() { return fut.await; } let uid = uid.await?; // check workspace permission if let Some(workspace_id) = workspace_id { if let Some(workspace_ac) = services.get(&AccessResource::Workspace) { if let Err(err) = workspace_ac .check_resource_permission(&uid, &workspace_id.to_string(), method.clone(), &path) .await { error!("workspace access control: {}", err,); return Err(Error::from(err)); } }; } // check collab permission if let Some(collab_object_id) = object_id { if let Some(collab_ac) = services.get(&AccessResource::Collab) { if let Err(err) = collab_ac .check_resource_permission(&uid, &collab_object_id, method, &path) .await { error!( "collab access control: {:?}, with path:{}", err, path.as_str() ); return Err(Error::from(err)); } }; } // call next service fut.await }) }, } } }