mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-21 08:02:26 +00:00
5c08abe141
* feat(aiagent): handle oauth for mcp tools * cleaning * cleaning * move oauth2 * cleaning * cimd * wrap path * fix frontend * fix * refresh resources * cleaning * update ee ref * update sqlx * cleaning * update * cleaning * update ref * cleaning * cleaning * update ref
233 lines
7.0 KiB
Rust
233 lines
7.0 KiB
Rust
/*
|
|
* Author: Ruben Fiszel
|
|
* Copyright: Windmill Labs, Inc 2022
|
|
* This file and its contents are licensed under the AGPLv3 License.
|
|
* Please see the included NOTICE for copyright information and
|
|
* LICENSE-AGPL for a copy of the license.
|
|
*/
|
|
|
|
use axum::body::Body;
|
|
use axum::response::Response;
|
|
use http::StatusCode;
|
|
|
|
lazy_static::lazy_static! {
|
|
pub static ref PUBLIC_APP_DOMAIN: Option<String> = std::env::var("PUBLIC_APP_DOMAIN").ok();
|
|
}
|
|
|
|
/// Middleware to restrict public app domain to whitelisted routes
|
|
pub async fn public_app_domain_filter(
|
|
req: axum::extract::Request,
|
|
next: axum::middleware::Next,
|
|
) -> Response {
|
|
if let Some(public_domain) = PUBLIC_APP_DOMAIN.as_ref() {
|
|
if let Some(host) = req.headers().get(http::header::HOST) {
|
|
if let Ok(host_str) = host.to_str() {
|
|
// Extract hostname without port
|
|
let hostname = host_str.split(':').next().unwrap_or(host_str);
|
|
|
|
if hostname == public_domain {
|
|
let path = req.uri().path();
|
|
|
|
// Check if route is whitelisted
|
|
let is_whitelisted = is_public_route_whitelisted(path);
|
|
|
|
if !is_whitelisted {
|
|
tracing::warn!(
|
|
"Rejected request to {} on public app domain {}",
|
|
path,
|
|
public_domain
|
|
);
|
|
return Response::builder()
|
|
.status(StatusCode::FORBIDDEN)
|
|
.body(Body::from(
|
|
"Access forbidden: route not allowed on public app domain",
|
|
))
|
|
.unwrap();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
next.run(req).await
|
|
}
|
|
|
|
fn is_public_route_whitelisted(path: &str) -> bool {
|
|
// Whitelisted route patterns for public app domain
|
|
let whitelist = [
|
|
// Public app routes
|
|
"/api/ee_license",
|
|
"/api/w/*/users/whoami",
|
|
"/api/w/*/apps_u/*",
|
|
"/api/w/*/resources/list",
|
|
"/api/w/*/jobs_u/getupdate_sse/*",
|
|
"/api/auth/login",
|
|
"/api/w/*/folders/listnames",
|
|
"/api/w/*/resources/exists/*",
|
|
"/api/w/*/resources/type/get/*",
|
|
"/api/w/*/resources/type/listnames",
|
|
"/api/oauth/login/*",
|
|
"/api/oauth/connect/*",
|
|
"/oauth/callback/*",
|
|
"/api/mcp/oauth/callback",
|
|
"/user/login_callback/*",
|
|
"/api/workspaces/users",
|
|
"/api/users/whoami",
|
|
"/api/apps_u/*",
|
|
"/api/oauth/list_connects",
|
|
"/api/oauth/list_logins",
|
|
"/public/*",
|
|
"/a/*",
|
|
"/api/oauth/get_connect/*",
|
|
"/Inter-Variable.woff2",
|
|
];
|
|
|
|
for pattern in &whitelist {
|
|
if matches_pattern(path, pattern) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
fn matches_pattern(path: &str, pattern: &str) -> bool {
|
|
// Handle exact match
|
|
if path == pattern {
|
|
return true;
|
|
}
|
|
|
|
// Handle wildcard patterns
|
|
if !pattern.contains('*') {
|
|
return false;
|
|
}
|
|
|
|
let parts: Vec<&str> = pattern.split('*').collect();
|
|
|
|
// Start from the beginning - path must start with first part
|
|
if !path.starts_with(parts[0]) {
|
|
return false;
|
|
}
|
|
|
|
let mut remaining = &path[parts[0].len()..];
|
|
|
|
// Check each part in between wildcards
|
|
for i in 1..parts.len() - 1 {
|
|
let part = parts[i];
|
|
if let Some(pos) = remaining.find(part) {
|
|
remaining = &remaining[pos + part.len()..];
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Check the last part
|
|
let last = parts[parts.len() - 1];
|
|
if last.is_empty() {
|
|
// Wildcard at the end, already matched
|
|
return true;
|
|
} else {
|
|
// Must end with the last part
|
|
return remaining.ends_with(last);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_matches_pattern_exact() {
|
|
assert!(matches_pattern("/api/version", "/api/version"));
|
|
assert!(!matches_pattern("/api/versions", "/api/version"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_matches_pattern_wildcard_middle() {
|
|
assert!(matches_pattern(
|
|
"/api/w/my-workspace/apps_u/my-app",
|
|
"/api/w/*/apps_u/*"
|
|
));
|
|
assert!(matches_pattern(
|
|
"/api/w/workspace123/jobs_u/job456",
|
|
"/api/w/*/jobs_u/*"
|
|
));
|
|
assert!(!matches_pattern(
|
|
"/api/w/workspace/apps/my-app",
|
|
"/api/w/*/apps_u/*"
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn test_matches_pattern_wildcard_end() {
|
|
assert!(matches_pattern("/api/r/some/path", "/api/r/*"));
|
|
assert!(matches_pattern("/api/auth/login", "/api/auth/*"));
|
|
assert!(matches_pattern("/public/asset.css", "/public/*"));
|
|
assert!(!matches_pattern("/api/admin/users", "/api/r/*"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_is_public_route_whitelisted() {
|
|
// Whitelisted routes based on current whitelist
|
|
assert!(is_public_route_whitelisted("/api/ee_license"));
|
|
assert!(is_public_route_whitelisted(
|
|
"/api/w/my-workspace/users/whoami"
|
|
));
|
|
assert!(is_public_route_whitelisted(
|
|
"/api/w/my-workspace/apps_u/my-app"
|
|
));
|
|
assert!(is_public_route_whitelisted(
|
|
"/api/w/workspace/resources/list"
|
|
));
|
|
|
|
// Non-whitelisted routes
|
|
assert!(!is_public_route_whitelisted("/api/version"));
|
|
assert!(!is_public_route_whitelisted("/api/uptodate"));
|
|
assert!(!is_public_route_whitelisted("/api/w/workspace/scripts"));
|
|
assert!(!is_public_route_whitelisted("/api/w/workspace/flows"));
|
|
assert!(!is_public_route_whitelisted(
|
|
"/api/w/workspace/apps/private"
|
|
));
|
|
assert!(!is_public_route_whitelisted("/api/admin/settings"));
|
|
assert!(!is_public_route_whitelisted("/api/users"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_matches_pattern_complex_multiple_wildcards() {
|
|
// Test pattern with two wildcards like "/api/w/*/foo/*"
|
|
assert!(matches_pattern(
|
|
"/api/w/workspace1/s3_proxy/file.txt",
|
|
"/api/w/*/s3_proxy/*"
|
|
));
|
|
assert!(matches_pattern(
|
|
"/api/w/my-workspace/s3_proxy/path/to/file.jpg",
|
|
"/api/w/*/s3_proxy/*"
|
|
));
|
|
assert!(matches_pattern(
|
|
"/api/w/test/apps_u/myapp",
|
|
"/api/w/*/apps_u/*"
|
|
));
|
|
|
|
// Should not match if middle part is different
|
|
assert!(!matches_pattern(
|
|
"/api/w/workspace1/other/file.txt",
|
|
"/api/w/*/s3_proxy/*"
|
|
));
|
|
assert!(!matches_pattern(
|
|
"/api/w/workspace/apps/myapp",
|
|
"/api/w/*/apps_u/*"
|
|
));
|
|
|
|
// Edge cases
|
|
assert!(matches_pattern(
|
|
"/api/w/ws/s3_proxy/a",
|
|
"/api/w/*/s3_proxy/*"
|
|
));
|
|
|
|
// Path must have content after the last wildcard segment
|
|
assert!(matches_pattern(
|
|
"/api/w/ws/s3_proxy/",
|
|
"/api/w/*/s3_proxy/*"
|
|
));
|
|
}
|
|
}
|