aboutsummaryrefslogtreecommitdiffstats
path: root/src/api/dashboard
diff options
context:
space:
mode:
Diffstat (limited to 'src/api/dashboard')
-rw-r--r--src/api/dashboard/mod.rs72
1 files changed, 69 insertions, 3 deletions
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 } } });