diff options
| -rw-r--r-- | docs/access.md | 3 | ||||
| -rw-r--r-- | src/api/dashboard/mod.rs | 72 |
2 files changed, 71 insertions, 4 deletions
diff --git a/docs/access.md b/docs/access.md index cd11723..7a85c94 100644 --- a/docs/access.md +++ b/docs/access.md @@ -55,7 +55,8 @@ change to it counts on a node as soon as it has reached it. * **Protocol data** is in the store too, under `_continuum`: `read` and `write` on it as on any path. * **Looking at the node** (`/admin/stats`, `GET /peers`, the dashboard): any user with - a capability. + a capability. The dashboard's document box is read through the store, as the user + may; the page is told only of changes where its user may read. * **Remakes** (`/admin/remakes`): `read` on `_continuum/remakes` to look, `write` to propose or abort one. **Forgetting a peer** (`DELETE /peers/…`): `write` on `_continuum/forgotten`. diff --git a/src/api/dashboard/mod.rs b/src/api/dashboard/mod.rs index bea5493..b89955a 100644 --- a/src/api/dashboard/mod.rs +++ b/src/api/dashboard/mod.rs @@ -100,7 +100,8 @@ struct Hub { /// Each patch, to the clients connected. patches: broadcast::Sender<Arc<Published>>, /// Where the document changed since the last tick, to the clients connected: - /// `{"paths": [...]}`, or `{"all": true}` if too many or not known. + /// `{"paths": [...]}`, or `{"all": true}` if too many or not known. Shared by every page + /// open; each is told of the paths its user may read at least partly (`changed_for`). documents: broadcast::Sender<Arc<String>>, /// The messages exchanged since the last tick, one by one (`traffic::MessageLog`), /// to the clients connected: a JSON list. @@ -672,7 +673,10 @@ async fn current_state(Extension(hub): Extension<Arc<Hub>>) -> Response { /// changed (`Hub::documents`, at most every tick), `messages` with the messages since /// the last tick (`Hub::messages`), and `offline` when the node stops, which ends the /// stream. -async fn events(Extension(hub): Extension<Arc<Hub>>) -> impl IntoResponse { +async fn events( + Extension(hub): Extension<Arc<Hub>>, + Extension(access): Extension<super::access::Access>, +) -> impl IntoResponse { // Subscribed before the snapshot is read: no patch after it is missed. let mut patches = hub.patches.subscribe(); let mut documents = hub.documents.subscribe(); @@ -696,7 +700,9 @@ async fn events(Extension(hub): Extension<Arc<Hub>>) -> impl IntoResponse { let received = match next { Next::Patch(received) => received, Next::Document(Ok(changed)) => { - yield Ok(Event::default().event("document").data(changed.as_str())); + if let Some(changed) = changed_for(&changed, &access) { + yield Ok(Event::default().event("document").data(changed)); + } continue; } Next::Document(Err(broadcast::error::RecvError::Lagged(_))) => { @@ -756,6 +762,36 @@ fn snapshot_event(published: &Published) -> Event { .data(published.json()) } +/// A `document` event (`Hub::documents`) as the page of a user with `access` is told +/// it: of the changed paths, those it may read at least partly (`access::Visible`), so +/// that it isn't told where in the document others write what it may not read. None if +/// none is left. A few prefix comparisons per path and capability, once a tick per +/// page. +fn changed_for(changed: &str, access: &super::access::Access) -> Option<String> { + use super::access::{Access, Visible}; + if matches!(access, Access::Everything) { + return Some(changed.to_owned()); + } + let mut parsed: JsonValue = serde_json::from_str(changed).ok()?; + let Some(paths) = parsed.get_mut("paths").and_then(JsonValue::as_array_mut) else { + // `{"all": true}`: the page reads again what it shows, as its user may. + return Some(changed.to_owned()); + }; + paths.retain(|path| { + let path: Vec<String> = path + .as_array() + .map(|segments| { + segments + .iter() + .filter_map(|segment| segment.as_str().map(str::to_owned)) + .collect() + }) + .unwrap_or_default(); + access.visible(&path) != Visible::Nothing + }); + (!paths.is_empty()).then(|| parsed.to_string()) +} + #[cfg(test)] mod tests { use super::*; @@ -803,6 +839,36 @@ mod tests { } #[test] + fn each_page_is_told_of_what_its_user_may_read() { + use super::super::access::Access; + use crate::access::{Capability, Caps}; + let reader = Access::Caps(Caps::new(vec![Capability { + path: vec!["servers".to_owned(), "eu".to_owned()], + rights: ["read".to_owned()].into(), + by: String::new(), + since: 0, + }])); + let changed = json!({ "paths": [["servers", "eu", "a"], ["servers", "us"], ["servers"]] }) + .to_string(); + let told: JsonValue = + serde_json::from_str(&changed_for(&changed, &reader).unwrap()).unwrap(); + assert_eq!( + told, + json!({ "paths": [["servers", "eu", "a"], ["servers"]] }) + ); + let elsewhere = json!({ "paths": [["accounts"]] }).to_string(); + assert_eq!(changed_for(&elsewhere, &reader), None); + assert_eq!( + changed_for(r#"{"all":true}"#, &reader).as_deref(), + Some(r#"{"all":true}"#) + ); + assert_eq!( + changed_for(&elsewhere, &Access::Everything), + Some(elsewhere.clone()) + ); + } + + #[test] fn ages_alone_are_no_change() { let old = json!({ "uptimeSecs": 1, "peers": { "p": { "lastSeenSecsAgo": 3, "rttMs": 2 } } }); |
