@@ -25,13 +25,13 @@ use crate::notifications::outbox::NOTIFICATION_OUTBOX_DRAIN_BATCH_SIZE;
2525use crate :: notifications:: placement:: resolve_inbox_holder;
2626use crate :: notifications:: protocol:: { NotificationTransportMessage , notification_message_kind} ;
2727use crate :: notifications:: unread:: { UnreadCountInput , UnreadCountOperation } ;
28+ use crate :: notifications:: watch:: authorization:: list_authorized_watch_subscriptions;
2829use crate :: notifications:: watch:: expand:: expand_watch_events;
2930use crate :: notifications:: watch:: interest:: {
3031 mark_watch_interest_dirty, schedule_watch_interest_publish,
3132} ;
3233use crate :: notifications:: watch:: subscriptions:: {
3334 create_replicated_watch_subscription, delete_replicated_watch_subscription,
34- list_watch_subscriptions,
3535} ;
3636
3737const NOTIFICATION_MAX_FUTURE_SKEW_MS : u64 = 5 * 60 * 1000 ;
@@ -245,9 +245,9 @@ async fn build_response(
245245 return NotificationTransportMessage :: Reject ( reason) ;
246246 }
247247
248- match list_watch_subscriptions ( & context. storage_handle , owner) . await {
248+ match list_authorized_watch_subscriptions ( context, owner) . await {
249249 Ok ( subscriptions) => NotificationTransportMessage :: WatchList { subscriptions } ,
250- Err ( error) => NotificationTransportMessage :: Reject ( error. to_string ( ) ) ,
250+ Err ( error) => NotificationTransportMessage :: Reject ( error) ,
251251 }
252252 }
253253 NotificationTransportMessage :: DeliverWatchEvents { events } => {
@@ -653,7 +653,9 @@ mod tests {
653653 unread_count_remote,
654654 } ;
655655 use crate :: notifications:: inbox:: upsert_inbox_records;
656- use crate :: notifications:: watch:: subscriptions:: create_watch_subscription;
656+ use crate :: notifications:: watch:: subscriptions:: {
657+ WATCH_SUBSCRIPTION_UNAUTHORIZED , create_watch_subscription, list_watch_subscriptions,
658+ } ;
657659 use aruna_core:: keyspaces:: {
658660 AUTH_KEYSPACE , NOTIFICATION_INBOX_KEYSPACE , NOTIFICATION_WATCH_INTEREST_KEYSPACE ,
659661 } ;
@@ -1446,6 +1448,7 @@ mod tests {
14461448 . await ;
14471449
14481450 let owner = recipient_for_holder ( & config, b. net . node_id ( ) , realm_id) ;
1451+ install_watch_authorization ( & b, realm_id, owner, & [ ] ) . await ;
14491452 let mask = WatchEventMask :: from_kinds ( [ WatchEventKind :: DataUploaded ] ) ;
14501453 let prefix = data_path ( "prefix" ) ;
14511454 let created = create_watch_remote ( & a. net , b. net . node_id ( ) , owner, prefix. clone ( ) , mask)
@@ -1525,6 +1528,104 @@ mod tests {
15251528 ) ;
15261529 }
15271530
1531+ // The holder persists and replicates the subscription, so a proxying peer's
1532+ // assertion is not authority to watch: an owner without READ is refused there
1533+ // too, and nothing durable is written.
1534+ #[ tokio:: test]
1535+ async fn create_requires_read ( ) {
1536+ let realm_id = RealmId :: from_bytes ( [ 86u8 ; 32 ] ) ;
1537+ let a = spawn ( realm_id, [ 86u8 ; 32 ] ) . await ;
1538+ let b = spawn ( realm_id, [ 87u8 ; 32 ] ) . await ;
1539+ connect ( & a, & b) . await ;
1540+ let config = install_config (
1541+ & b,
1542+ realm_id,
1543+ & [
1544+ ( a. net . node_id ( ) , RealmNodeKind :: Server ) ,
1545+ ( b. net . node_id ( ) , RealmNodeKind :: Server ) ,
1546+ ] ,
1547+ )
1548+ . await ;
1549+
1550+ let owner = recipient_for_holder ( & config, b. net . node_id ( ) , realm_id) ;
1551+ // The watched group exists and is readable, just not by `owner`.
1552+ install_watch_authorization ( & b, realm_id, UserId :: new ( Ulid :: r#gen ( ) , realm_id) , & [ ] ) . await ;
1553+
1554+ let error = create_watch_remote (
1555+ & a. net ,
1556+ b. net . node_id ( ) ,
1557+ owner,
1558+ data_path ( "prefix" ) ,
1559+ WatchEventMask :: from_kinds ( [ WatchEventKind :: DataUploaded ] ) ,
1560+ )
1561+ . await
1562+ . expect_err ( "an owner without READ must be refused by the holder" ) ;
1563+ assert_eq ! ( error, WATCH_SUBSCRIPTION_UNAUTHORIZED ) ;
1564+ assert ! (
1565+ list_watch_subscriptions( & b. context. storage_handle, owner)
1566+ . await
1567+ . expect( "list succeeds" )
1568+ . is_empty( ) ,
1569+ "an unauthorized create must not persist watch state"
1570+ ) ;
1571+ }
1572+
1573+ // Enumeration shares the delivery authorization result: once READ is revoked
1574+ // the watch stops being listed, though the stored row itself survives.
1575+ #[ tokio:: test]
1576+ async fn revoked_watch_unlisted ( ) {
1577+ let realm_id = RealmId :: from_bytes ( [ 84u8 ; 32 ] ) ;
1578+ let a = spawn ( realm_id, [ 84u8 ; 32 ] ) . await ;
1579+ let b = spawn ( realm_id, [ 85u8 ; 32 ] ) . await ;
1580+ connect ( & a, & b) . await ;
1581+ let config = install_config (
1582+ & b,
1583+ realm_id,
1584+ & [
1585+ ( a. net . node_id ( ) , RealmNodeKind :: Server ) ,
1586+ ( b. net . node_id ( ) , RealmNodeKind :: Server ) ,
1587+ ] ,
1588+ )
1589+ . await ;
1590+
1591+ let owner = recipient_for_holder ( & config, b. net . node_id ( ) , realm_id) ;
1592+ install_watch_authorization ( & b, realm_id, owner, & [ ] ) . await ;
1593+ let created = create_watch_remote (
1594+ & a. net ,
1595+ b. net . node_id ( ) ,
1596+ owner,
1597+ data_path ( "prefix" ) ,
1598+ WatchEventMask :: from_kinds ( [ WatchEventKind :: DataUploaded ] ) ,
1599+ )
1600+ . await
1601+ . expect ( "authorized create succeeds" ) ;
1602+ assert_eq ! (
1603+ list_watches_remote( & a. net, b. net. node_id( ) , owner)
1604+ . await
1605+ . expect( "list succeeds" ) ,
1606+ vec![ created]
1607+ ) ;
1608+
1609+ // Re-owning the group drops the original owner's READ.
1610+ install_watch_authorization ( & b, realm_id, UserId :: new ( Ulid :: r#gen ( ) , realm_id) , & [ ] ) . await ;
1611+
1612+ assert ! (
1613+ list_watches_remote( & a. net, b. net. node_id( ) , owner)
1614+ . await
1615+ . expect( "list after revocation succeeds" )
1616+ . is_empty( ) ,
1617+ "a watch whose READ was revoked must not be enumerable"
1618+ ) ;
1619+ assert_eq ! (
1620+ list_watch_subscriptions( & b. context. storage_handle, owner)
1621+ . await
1622+ . expect( "stored row survives" )
1623+ . len( ) ,
1624+ 1 ,
1625+ "the row is filtered at the surface, not silently deleted"
1626+ ) ;
1627+ }
1628+
15281629 fn upload_event ( realm_id : RealmId , actor : UserId , path : & str ) -> WatchEvent {
15291630 let ( _, key) = path. split_once ( '/' ) . expect ( "bucket/key fixture" ) ;
15301631 WatchEvent {
0 commit comments