Skip to content
This repository was archived by the owner on Mar 25, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7e336bc
digikey
awantoch Nov 6, 2025
ef7b792
Multi-Tenant + RBAC support
awantoch Nov 10, 2025
4364eb3
bump password min 8 => 12
awantoch Nov 10, 2025
80ccf05
log
awantoch Nov 11, 2025
2241a12
refactor: update with_db_name to use in-memory SQLite storage for tests
awantoch Nov 11, 2025
6cef58e
Merge branch 'main' of github.com:BeemFlow/beemflow into feat/multi-t…
awantoch Nov 18, 2025
0d49e4a
feat: Add maximum request body size constant and update handler for D…
awantoch Nov 18, 2025
e4b027f
Add security tests for multi-organization isolation and update schema…
awantoch Nov 20, 2025
d6f46d8
feat: Update dependencies by removing tower_governor and adding hyper…
awantoch Nov 20, 2025
a572e20
feat: Enhance security checks for role management to prevent privileg…
awantoch Nov 21, 2025
bc9a146
lint
awantoch Nov 21, 2025
2c37eeb
feat: Improve error handling and validation across multiple modules
awantoch Nov 21, 2025
2fe4d6b
feat: Enhance code quality by improving Clippy linting rules and erro…
awantoch Nov 21, 2025
ab6d33f
temp rm audit middleware until later pr
awantoch Nov 21, 2025
6aca900
lint
awantoch Nov 21, 2025
b7ef09d
cleanup
awantoch Nov 24, 2025
4f3fc92
tweak rbac frontend
awantoch Dec 2, 2025
b5dbfd7
tweak multi-tenant isolation for flows and steps
awantoch Dec 2, 2025
1ec0780
refactor complex type
awantoch Dec 3, 2025
d9c7808
enhance password validation with allowlist approach and improve OAuth…
awantoch Dec 8, 2025
59dec0d
Refactor sql to use query_as instead of try_get
awantoch Dec 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ sqlx = { version = "0.8", features = [
"chrono",
"json",
"migrate",
"macros",
] }

# CLI
Expand Down Expand Up @@ -104,6 +105,8 @@ urlencoding = "2.1"
# Cryptography
sha2 = "0.10"
hmac = "0.12"
bcrypt = "0.15"
aes-gcm = "0.10" # OAuth token encryption (AES-256-GCM)

# Utilities
itertools = "0.14"
Expand Down
18 changes: 16 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,26 @@ fmt-check:
cargo fmt -- --check

lint:
cargo clippy --all-targets --all-features -- -D warnings
cargo clippy --lib --all-features -- \
-D warnings \
-D clippy::unwrap_used \
-D clippy::expect_used \
-D clippy::panic \
-D clippy::indexing_slicing \
-D clippy::unwrap_in_result
cargo clippy --bins --all-features -- -D warnings

# Auto-fix all issues (format + clippy --fix)
fix:
cargo fix --allow-dirty --allow-staged
cargo clippy --fix --allow-dirty --allow-staged
cargo clippy --fix --allow-dirty --allow-staged --lib --all-features -- \
-D warnings \
-D clippy::unwrap_used \
-D clippy::expect_used \
-D clippy::panic \
-D clippy::indexing_slicing \
-D clippy::unwrap_in_result
cargo clippy --fix --allow-dirty --allow-staged --bins --all-features -- -D warnings
$(MAKE) fmt

# ────────────────────────────────────────────────────────────────────────────
Expand Down
49 changes: 39 additions & 10 deletions core_macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,12 @@ fn to_snake_case(s: &str) -> String {
if !result.is_empty() {
result.push('_');
}
result.push(ch.to_lowercase().next().unwrap());
// Safe: to_lowercase() always returns at least one character
if let Some(lowercase) = ch.to_lowercase().next() {
result.push(lowercase);
} else {
result.push(ch);
}
} else {
result.push(ch);
}
Expand All @@ -246,10 +251,9 @@ fn to_snake_case(s: &str) -> String {
/// Parse HTTP method and path from string like "GET /flows/{name}"
fn parse_http_route(http: &str) -> (String, String) {
let parts: Vec<&str> = http.splitn(2, ' ').collect();
if parts.len() == 2 {
(parts[0].to_string(), parts[1].to_string())
} else {
("GET".to_string(), http.to_string())
match (parts.first(), parts.get(1)) {
(Some(&method), Some(&path)) => (method.to_string(), path.to_string()),
_ => ("GET".to_string(), http.to_string()),
}
}

Expand Down Expand Up @@ -299,6 +303,7 @@ fn generate_http_route_method(
}
} else if path_params.len() == 1 && (http_method == "GET" || http_method == "DELETE") {
// Single path param with GET/DELETE - construct input from path param only
#[allow(clippy::indexing_slicing)] // Safe: checked path_params.len() == 1 above
let param = &path_params[0];
let param_ident = Ident::new(param, Span::call_site());
(
Expand All @@ -317,7 +322,8 @@ fn generate_http_route_method(
.map(|p| Ident::new(p, Span::call_site()))
.collect();

let extractor = if path_params.len() == 1 {
let extractor = if param_idents.len() == 1 {
#[allow(clippy::indexing_slicing)] // Safe: checked param_idents.len() == 1
let param = &param_idents[0];
quote! {
axum::extract::Path(#param): axum::extract::Path<String>,
Expand Down Expand Up @@ -359,17 +365,40 @@ fn generate_http_route_method(
(quote! {}, quote! { () })
};

// Generate handler parameters with Extension extractor for RequestContext
// Extension<T> extracts per-request state inserted by middleware
// HTTP API routes are protected by auth middleware, so RequestContext is always present
let handler_params = if !matches!(extractors.to_string().as_str(), "") {
quote! {
axum::extract::Extension(req_ctx): axum::extract::Extension<crate::auth::RequestContext>,
#extractors
}
} else {
quote! {
axum::extract::Extension(req_ctx): axum::extract::Extension<crate::auth::RequestContext>
}
};

quote! {
/// Auto-generated HTTP route registration for this operation
pub fn http_route(deps: std::sync::Arc<super::Dependencies>) -> axum::Router {
axum::Router::new().route(
Self::HTTP_PATH.unwrap(),
axum::routing::#method_ident({
move |#extractors| async move {
move |#handler_params| async move {
let op = Self::new(deps.clone());
let result = op.execute(#input_construction).await
.map_err(|e| crate::http::AppError::from(e))?;
Ok::<axum::Json<_>, crate::http::AppError>(axum::Json(result))

// Construct input (may fail with validation errors)
let input = #input_construction;

// Execute with RequestContext in task-local storage
// This makes the context available to the operation via REQUEST_CONTEXT.try_with()
crate::core::REQUEST_CONTEXT.scope(req_ctx, async move {
op.execute(input).await
})
.await
.map(|output| axum::Json(output))
.map_err(|e| crate::http::AppError::from(e))
}
})
)
Expand Down
8 changes: 1 addition & 7 deletions docs/AUTH_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -1210,13 +1210,7 @@ pub async fn tenant_middleware(
request_id: uuid::Uuid::new_v4().to_string(),
};

// 6. Set PostgreSQL session variable for RLS
state.storage
.set_tenant_context(&request_context.tenant.tenant_id)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

// 7. Inject into request extensions
// 6. Inject into request extensions
req.extensions_mut().insert(request_context);

Ok(next.run(req).await)
Expand Down
18 changes: 0 additions & 18 deletions docs/AUTH_SAAS_PHASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1009,9 +1009,6 @@ pub trait AuthStorage: Send + Sync {
async fn get_tenant_secret(&self, tenant_id: &str, key: &str) -> Result<Option<TenantSecret>>;
async fn list_tenant_secrets(&self, tenant_id: &str) -> Result<Vec<TenantSecret>>;
async fn delete_tenant_secret(&self, tenant_id: &str, key: &str) -> Result<()>;

// PostgreSQL-specific: Set tenant context for RLS
async fn set_tenant_context(&self, tenant_id: &str) -> Result<()>;
}
```

Expand Down Expand Up @@ -1111,15 +1108,6 @@ impl AuthStorage for PostgresStorage {
}

// ... implement remaining methods

async fn set_tenant_context(&self, tenant_id: &str) -> Result<()> {
sqlx::query("SET LOCAL app.current_tenant_id = $1")
.bind(tenant_id)
.execute(&self.pool)
.await?;

Ok(())
}
}

// Helper struct for database rows
Expand Down Expand Up @@ -1733,12 +1721,6 @@ pub async fn tenant_middleware(
request_id,
};

// Set PostgreSQL session variables for RLS
if let Err(e) = state.storage.set_tenant_context(&req_ctx.tenant_id).await {
tracing::error!("Failed to set tenant context: {}", e);
// Continue anyway - RLS will catch any issues
}

req.extensions_mut().insert(req_ctx);

Ok(next.run(req).await)
Expand Down
Loading