use salvo::handler;
use salvo::http::StatusCode;
use salvo::prelude::*;
use sqlx::SqlitePool;
use tracing::{debug, warn};
use crate::acl;
use crate::db::tenants::Tenant;
#[handler]
pub async fn acl_check(
req: &mut Request,
depot: &mut Depot,
res: &mut Response,
ctrl: &mut FlowCtrl,
) {
let _auth_config = match depot.obtain::<crate::config::AuthConfig>() {
Ok(config) if config.enabled => config.clone(),
_ => return,
};
let tenant_id = match depot.get::<String>("tenant_id") {
Ok(id) => id.clone(),
Err(_) => {
res.status_code(StatusCode::UNAUTHORIZED);
res.render(Text::Plain("Authentication required"));
ctrl.skip_rest();
return;
}
};
let pool = match depot.obtain::<SqlitePool>() {
Ok(p) => p.clone(),
Err(_) => {
res.status_code(StatusCode::INTERNAL_SERVER_ERROR);
res.render(Text::Plain("Database not available"));
ctrl.skip_rest();
return;
}
};
let tenant = match Tenant::by_id(&pool, &tenant_id).await {
Ok(Some(t)) if t.enabled => t,
Ok(Some(_)) => {
res.status_code(StatusCode::FORBIDDEN);
res.render(Text::Plain("Account disabled"));
ctrl.skip_rest();
return;
}
Ok(None) => {
res.status_code(StatusCode::UNAUTHORIZED);
res.render(Text::Plain("Tenant not found"));
ctrl.skip_rest();
return;
}
Err(e) => {
warn!("Database error loading tenant: {}", e);
res.status_code(StatusCode::INTERNAL_SERVER_ERROR);
res.render(Text::Plain("Internal error"));
ctrl.skip_rest();
return;
}
};
let permissions = match tenant.permissions(&pool).await {
Ok(p) => p,
Err(e) => {
warn!("Failed to load permissions for tenant {}: {}", tenant_id, e);
res.status_code(StatusCode::INTERNAL_SERVER_ERROR);
res.render(Text::Plain("Internal error"));
ctrl.skip_rest();
return;
}
};
let resource = derive_resource_from_request(req);
debug!(
"ACL check: tenant={}, resource={}, permissions={}",
tenant_id,
resource,
permissions.len()
);
if acl::evaluate(&permissions, &resource) {
debug!("ACL granted: {} -> {}", tenant_id, resource);
depot.insert("acl_permissions", permissions);
} else {
debug!("ACL denied: {} -> {}", tenant_id, resource);
res.status_code(StatusCode::FORBIDDEN);
res.render(Text::Plain("Insufficient permissions"));
ctrl.skip_rest();
}
}
fn derive_resource_from_request(req: &Request) -> String {
let path = req.uri().path();
let method = req.method().as_str();
let action = match method {
"GET" | "HEAD" => "read",
"POST" => "write",
"PUT" | "PATCH" => "write",
"DELETE" => "delete",
_ => "read",
};
let segments: Vec<&str> = path
.trim_start_matches("/api/manage/")
.split('/')
.filter(|s| !s.is_empty())
.collect();
match segments.as_slice() {
["tenants"] => format!("tenants:*:{}", action),
["tenants", id] => format!("tenants:{}:{}", id, action),
["tenants", id, "plugins", ..] => format!("tenants:{}:configure", id),
["tenants", id, "ip-mappings", ..] => format!("tenants:{}:configure", id),
["groups"] => format!("groups:*:{}", action),
["groups", id] => format!("groups:{}:{}", id, action),
["groups", id, "members"] => format!("groups:{}:manage", id),
["groups", id, "permissions"] => format!("groups:{}:manage", id),
_ => format!("unknown:*:{}", action),
}
}