security(external_user): protect unnecessary route access to external users
This commit is contained in:
@@ -21,11 +21,17 @@
|
||||
//! [`super::admin`] — that variant exists because some handlers take
|
||||
//! `headers: HeaderMap` directly instead of `AuthUser`.
|
||||
|
||||
use axum::extract::{Request, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::middleware::Next;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::application::services::auth_application_service::AuthApplicationService;
|
||||
use crate::common::di::AppState;
|
||||
use crate::interfaces::errors::AppError;
|
||||
use crate::interfaces::middleware::auth::CurrentUser;
|
||||
|
||||
/// Require the caller to be an internal user. Returns `Ok(())` for
|
||||
/// internal callers, `Err(403)` for externals.
|
||||
@@ -90,3 +96,55 @@ pub async fn require_admin_user(
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Axum middleware layer that blocks external users from a whole route
|
||||
/// subtree. Apply via `.layer(from_fn_with_state(state, require_internal_user_layer))`
|
||||
/// on the protocol nests (CalDAV / CardDAV / WebDAV) that have no
|
||||
/// semantic meaning for externals — they own no calendars, no address
|
||||
/// books, no home folder.
|
||||
///
|
||||
/// Must run AFTER the auth middleware so `CurrentUser` is in the
|
||||
/// request extensions; in tower order that means the auth layer is
|
||||
/// added LAST (outermost). If the layer fires on an unauthenticated
|
||||
/// path (no `CurrentUser` populated), it simply passes through — the
|
||||
/// inner handler is then responsible for the 401, and we don't blanket-
|
||||
/// 403 traffic the auth layer would have rejected anyway.
|
||||
///
|
||||
/// Emits an `authz.external_user_blocked` audit event on rejection so
|
||||
/// operators can spot which surfaces externals are probing.
|
||||
pub async fn require_internal_user_layer(
|
||||
State(state): State<Arc<AppState>>,
|
||||
request: Request,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
let caller_id = request
|
||||
.extensions()
|
||||
.get::<Arc<CurrentUser>>()
|
||||
.map(|cu| cu.id);
|
||||
|
||||
let (Some(caller_id), Some(svc)) = (
|
||||
caller_id,
|
||||
state
|
||||
.auth_service
|
||||
.as_ref()
|
||||
.map(|s| &*s.auth_application_service),
|
||||
) else {
|
||||
// No auth populated, or auth disabled globally — pass through.
|
||||
return next.run(request).await;
|
||||
};
|
||||
|
||||
if let Err(err) = require_internal_user(svc, caller_id).await {
|
||||
let path = request.uri().path().to_owned();
|
||||
tracing::info!(
|
||||
target: "audit",
|
||||
event = "authz.external_user_blocked",
|
||||
reason = "internal_only_surface",
|
||||
caller_id = %caller_id,
|
||||
path = %path,
|
||||
"👮🏻♂️ External user blocked from internal-only route subtree"
|
||||
);
|
||||
return err.into_response();
|
||||
}
|
||||
|
||||
next.run(request).await
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user