aboutsummaryrefslogtreecommitdiffstats
path: root/src/api/store.rs
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-07 22:35:09 +0100
committertv <tv@krebsco.de>2026-11-07 22:35:09 +0100
commitf2487f5a1f897bb5499ffd782562f53967f20606 (patch)
tree3e6b64f4ef1ebe1c19e346c2080a5a7afb31ec26 /src/api/store.rs
parentc82a62186a8e0a46fd41e08345a2fa3e54ddffe2 (diff)
access: keeping the swarm manageable
A write that would leave its own writer unable to manage users (deleting, disabling or narrowing its entry, removing the registry) is refused; --protocol-writers can set a registry right that nobody may manage any more. Auth plan M1.
Diffstat (limited to 'src/api/store.rs')
-rw-r--r--src/api/store.rs114
1 files changed, 96 insertions, 18 deletions
diff --git a/src/api/store.rs b/src/api/store.rs
index 1ab3951..e1f9b53 100644
--- a/src/api/store.rs
+++ b/src/api/store.rs
@@ -385,10 +385,13 @@ pub(crate) async fn put_store_entry(
let writing = store::Writing::new(body.sticky, body.timestamps, query.retype);
// A value that can't be shown as asked is refused, and the write with it.
+ let writer = user.0.clone();
let ((retyped, read), heads) = state
.mutate_doc_for(Some(user.0.clone()), move |doc| {
+ let before = Before::of(&*doc, &writer);
let retyped = store::write_val_path(doc, &path, &body.value, writing)
.map_err(|e| store_status(e, "Failed to write store value"))?;
+ registry_kept(&*doc, &path, before, &writer)?;
// What was written, shown back to whoever wrote it.
let read = read_shown(&*doc, &path, accept.as_ref(), false, &Access::Everything)?;
Ok::<_, ApiError>((retyped, read))
@@ -465,6 +468,70 @@ fn parse_write_path(
Ok(path)
}
+/// The registry as a write found it (`registry_kept`): whether it existed, and whether
+/// the writing user could manage users (`registry::managers`).
+#[derive(Clone, Copy)]
+struct Before {
+ existed: bool,
+ managed: bool,
+}
+
+impl Before {
+ fn of(doc: &impl crate::document::ReadDoc, user: &str) -> Self {
+ use crate::access::registry;
+ let existed = registry::exists(doc);
+ Self {
+ existed,
+ managed: existed
+ && registry::caps_of(doc, user)
+ .allows(crate::access::Right::Write, &registry::users_dir()),
+ }
+ }
+}
+
+/// `409` for a write to `path` by `user` that would lock that user out of managing
+/// users: one that could and no longer may (deleting, disabling or narrowing its own
+/// entry), and the write that makes the registry, if it doesn't make `user` one who may
+/// (from then on only registered users are served). And for one that would remove
+/// the registry, back to a swarm without one. Checked in the write's transaction,
+/// after it: the write is dropped whole. Others are not kept from removing the last who
+/// may manage users: no node could keep two such writes at once on two nodes from
+/// leaving nobody once they merge; then the users in `--protocol-writers` may do
+/// everything again (`access::authorize`).
+fn registry_kept(
+ doc: &impl crate::document::ReadDoc,
+ path: &[String],
+ before: Before,
+ user: &str,
+) -> Result<(), ApiError> {
+ use crate::access::registry;
+ if !store::paths_related(path, &registry::users_dir()) {
+ return Ok(());
+ }
+ let refuse = |why: &str| {
+ Err(ApiError(
+ StatusCode::CONFLICT,
+ Some(format!("{why} (docs/access.md)")),
+ ))
+ };
+ if !registry::exists(doc) {
+ return if before.existed {
+ refuse("this would remove the user registry")
+ } else {
+ Ok(())
+ };
+ }
+ let manages =
+ registry::caps_of(doc, user).allows(crate::access::Right::Write, &registry::users_dir());
+ if !before.existed && !manages {
+ return refuse("the registry's first users must include you, with write on _continuum");
+ }
+ if before.managed && !manages {
+ return refuse("this would keep you from managing users");
+ }
+ Ok(())
+}
+
/// How a value is shown, as the request's `Accept` asks.
#[derive(Clone, Copy, Debug)]
enum Shown {
@@ -718,12 +785,16 @@ pub(crate) async fn post_store_entry(
return Ok(redirect);
}
if let Some(by) = query.increment {
+ let writer = user.0.clone();
let (value, heads) = state
.mutate_doc_for(Some(user.0.clone()), move |doc| {
- store::increment_path(doc, &path, by)
+ let before = Before::of(&*doc, &writer);
+ let value = store::increment_path(doc, &path, by)
+ .map_err(|e| store_status(e, "Failed to increment a counter"))?;
+ registry_kept(&*doc, &path, before, &writer)?;
+ Ok::<_, ApiError>(value)
})
- .await
- .map_err(|e| store_status(e, "Failed to increment a counter"))?;
+ .await?;
return Ok(with_heads(
{
let counter = ScalarValue::counter(value);
@@ -750,10 +821,13 @@ pub(crate) async fn post_store_entry(
};
let accept = headers.get(ACCEPT).cloned();
// A value that can't be shown as asked is refused, and the cast with it.
+ let writer = user.0.clone();
let ((retyped, read), heads) = state
.mutate_doc_for(Some(user.0.clone()), move |doc| {
+ let before = Before::of(&*doc, &writer);
let retyped = store::cast_path(doc, &path, to, timestamps)
.map_err(|e| store_status(e, "Failed to cast a store value"))?;
+ registry_kept(&*doc, &path, before, &writer)?;
let read = read_shown(&*doc, &path, accept.as_ref(), false, &Access::Everything)?;
Ok::<_, ApiError>((retyped, read))
})
@@ -763,22 +837,23 @@ pub(crate) async fn post_store_entry(
}
let index = path.last().and_then(|leaf| store::list_index(leaf));
let value = body_value(&headers, &body)?;
- let inserted = state
+ let writer = user.0.clone();
+ let ((), heads) = state
.mutate_doc_for(Some(user.0.clone()), move |doc| {
+ let before = Before::of(&*doc, &writer);
store::insert_val_path(doc, &path, &value.value)
+ .map_err(|e| store_status(e, "Failed to insert into a list"))?;
+ registry_kept(&*doc, &path, before, &writer)
})
- .await;
- match inserted {
- Ok(((), heads)) => Ok(with_heads(
- (
- StatusCode::CREATED,
- axum::Json(serde_json::json!({ "index": index })),
- )
- .into_response(),
- &heads,
- )),
- Err(error) => Err(store_status(error, "Failed to insert into a list")),
- }
+ .await?;
+ Ok(with_heads(
+ (
+ StatusCode::CREATED,
+ axum::Json(serde_json::json!({ "index": index })),
+ )
+ .into_response(),
+ &heads,
+ ))
}
/// `DELETE /store/{key}`.
@@ -813,12 +888,15 @@ pub(crate) async fn delete_store_entry(
.find(|depth| access.allows(crate::access::Right::Write, &path[..*depth]))
.unwrap_or(path.len())
});
+ let writer = user.0.clone();
let ((), heads) = state
.mutate_doc_for(Some(user.0.clone()), move |doc| {
+ let before = Before::of(&*doc, &writer);
store::delete_path_pruning(doc, &path, prune)
+ .map_err(|e| store_status(e, "Failed to delete store value"))?;
+ registry_kept(&*doc, &path, before, &writer)
})
- .await
- .map_err(|e| store_status(e, "Failed to delete store value"))?;
+ .await?;
Ok(with_heads(StatusCode::NO_CONTENT.into_response(), &heads))
}