Skip to content

Commit 1e11995

Browse files
committed
fix: answer unreadable watch paths without an existence signal
1 parent 283e65a commit 1e11995

1 file changed

Lines changed: 69 additions & 6 deletions

File tree

api/src/routes/notifications.rs

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
use crate::auth::{ensure_permission, require_realm_auth};
1+
use crate::auth::require_realm_auth;
22
use crate::error::{ErrorResponse, ServerError, ServerResult};
33
use crate::server_state::ServerState;
44
use aruna_core::NodeId;
55
use aruna_core::UserId;
66
use aruna_core::structs::{
77
AuthContext, NOTIFICATION_WATCH_MAX_PREFIX_LEN, NotificationClass, NotificationKind,
8-
NotificationRecord, Permission, WatchEventKind, WatchEventMask, WatchSubscription,
8+
NotificationRecord, WatchEventKind, WatchEventMask, WatchSubscription,
99
};
1010
use aruna_operations::driver::DriverContext;
1111
use aruna_operations::notifications::dispatch::{
@@ -15,7 +15,9 @@ use aruna_operations::notifications::dispatch::{
1515
};
1616
use aruna_operations::notifications::list::LIST_NOTIFICATIONS_MAX_LIMIT;
1717
use aruna_operations::notifications::mark_read::MARK_READ_MAX_IDS;
18-
use aruna_operations::notifications::watch::authorization::watch_permission_path;
18+
use aruna_operations::notifications::watch::authorization::{
19+
is_watch_authorized, watch_permission_path,
20+
};
1921
use axum::extract::{Path, Query, State};
2022
use axum::http::StatusCode;
2123
use axum::response::sse::{Event, KeepAlive, Sse};
@@ -319,15 +321,32 @@ fn notification_response(record: &NotificationRecord) -> NotificationResponse {
319321
response
320322
}
321323

324+
/// Creation shares the exact authorization result delivery and enumeration use,
325+
/// so a watch cannot be created on anything they would later refuse. A prefix
326+
/// with no canonical resource identity is a malformed request; everything else a
327+
/// caller may not read answers Forbidden, whether or not it exists.
322328
async fn authorize_watch(
323329
state: &ServerState,
324330
auth: &AuthContext,
325331
path_prefix: &str,
326332
event_mask: WatchEventMask,
327333
) -> ServerResult<()> {
328-
let permission_path = watch_permission_path(state.get_realm_id(), path_prefix, event_mask)
329-
.ok_or(ServerError::BadRequest)?;
330-
ensure_permission(state, auth, permission_path, Permission::READ).await
334+
if watch_permission_path(state.get_realm_id(), path_prefix, event_mask).is_none() {
335+
return Err(ServerError::BadRequest);
336+
}
337+
match is_watch_authorized(
338+
&state.get_ctx(),
339+
state.get_realm_id(),
340+
auth.user_id,
341+
path_prefix,
342+
event_mask,
343+
)
344+
.await
345+
{
346+
Ok(true) => Ok(()),
347+
Ok(false) => Err(ServerError::Forbidden),
348+
Err(error) => Err(ServerError::InternalError(error)),
349+
}
331350
}
332351

333352
#[utoipa::path(
@@ -1565,6 +1584,50 @@ mod tests {
15651584
assert_eq!(created.path_prefix, path_prefix);
15661585
}
15671586

1587+
// A group that does not exist and a group the caller may not read answer
1588+
// identically, so creating a watch is never an existence oracle.
1589+
#[tokio::test]
1590+
async fn missing_group_forbidden() {
1591+
let realm_id = realm_id(18);
1592+
let holder = node(18);
1593+
let (_dir, state) = build_state(realm_id, holder).await;
1594+
install_local_holder_config(&state, realm_id, holder).await;
1595+
let caller = UserId::new(Ulid::r#gen(), realm_id);
1596+
let existing_group = Ulid::r#gen();
1597+
install_group_authorization(
1598+
&state,
1599+
realm_id,
1600+
existing_group,
1601+
UserId::new(Ulid::r#gen(), realm_id),
1602+
&[],
1603+
)
1604+
.await;
1605+
1606+
let missing = create_watch(
1607+
State(state.clone()),
1608+
Extension(Some(auth_for(caller, realm_id))),
1609+
Json(CreateWatchRequest {
1610+
path_prefix: format!("meta/{}/datasets", Ulid::r#gen()),
1611+
events: vec!["metadata_created".to_string()],
1612+
}),
1613+
)
1614+
.await
1615+
.expect_err("a watch on a group that does not exist must be refused");
1616+
assert!(matches!(missing, ServerError::Forbidden));
1617+
1618+
let unreadable = create_watch(
1619+
State(state),
1620+
Extension(Some(auth_for(caller, realm_id))),
1621+
Json(CreateWatchRequest {
1622+
path_prefix: format!("meta/{existing_group}/datasets"),
1623+
events: vec!["metadata_created".to_string()],
1624+
}),
1625+
)
1626+
.await
1627+
.expect_err("a watch on an existing unreadable group must be refused");
1628+
assert!(matches!(unreadable, ServerError::Forbidden));
1629+
}
1630+
15681631
#[tokio::test]
15691632
async fn create_watch_rejects_mixed_event_namespaces() {
15701633
let realm_id = realm_id(17);

0 commit comments

Comments
 (0)