M9: team links the list no longer hands out are removed at startup; a team link is made before the list changes

This commit is contained in:
Patrick Wozniak
2026-10-30 17:39:36 +01:00
parent 2d23f95673
commit d015436b13
5 changed files with 55 additions and 4 deletions
+1
View File
@@ -431,6 +431,7 @@ impl Store {
pub fn open(root: &Path, cfg: StoreConfig) -> Result<Store, String> {
let mut store = Self::try_open(root, cfg)?.ok_or_else(|| format!("another figcraft-server (or a command) is using {}", root.display()))?;
store.sweep();
store.drop_stale_team_links();
Ok(store)
}
+23 -1
View File
@@ -51,13 +51,14 @@ impl Store {
if let Some(made) = made {
return Ok(made);
}
// Made before the list changes: a failure leaves the links as they were.
let (mut link, token) = new_link(access, &label(access), now())?;
let before = entry.links.clone();
entry.links.retain(|l| l.team.is_none());
if entry.links.len() >= max {
entry.links = before;
return Err(format!("a file has at most {max} links: revoke one first"));
}
let (mut link, token) = new_link(access, &label(access), now())?;
link.team = Some(TeamToken(token.clone()));
entry.links.push(link.clone());
if let Err(e) = write_json(&path, &entry.links) {
@@ -68,6 +69,27 @@ impl Store {
Ok(NewLink { link: link.info(file), token })
}
/// At startup: the team links the list doesn't hand out now (it is off, or gives another
/// access) are removed for good, so setting `team_files` back doesn't bring them back. Nobody
/// keeps a team link's token for later (a team list link names none), so nothing is lost: the
/// next join makes a new one. One that can't be removed stays refused at the door, and the
/// operator is told.
pub(super) fn drop_stale_team_links(&mut self) {
let team = self.cfg.hosting.team;
let stale = |l: &super::Link| l.team.is_some() && team != Some(l.access);
let files: Vec<String> = self.files.iter().filter(|(_, e)| e.links.iter().any(stale)).map(|(id, _)| id.clone()).collect();
for file in files {
let path = self.dir(&file).join(LINKS);
let Some(entry) = self.files.get_mut(&file) else { continue };
entry.links.retain(|l| !stale(l));
if let Err(e) = write_json(&path, &entry.links) {
self.note(format!(
"file {file}: its old team links couldn't be removed ({e}); they are refused, but would work again if team_files were set back to their access"
));
}
}
}
/// `file` stopped being shared (a link was revoked): revoke its team links too. The ids
/// revoked (whoever joined with them is disconnected); none while it is still shared.
pub(super) fn team_unshared(&mut self, file: &str) -> Vec<String> {
+26 -1
View File
@@ -171,10 +171,35 @@ fn turning_the_list_off_or_changing_its_access_refuses_old_team_links() {
pump(&mut [&mut b], "Bob in, view only", |s| live(s[0]) && s[0].live_summary()["access"] == "view");
drop(b);
drop(server);
// Off: the list is gone and its links with it.
// Off: the list is gone and its links with it, also from the file's links.
let server = crate::start(&Config { open_publish: true, ..config(dir.path()) }).unwrap();
let mut b = join(&invite(server.addr(), &file, &view), "Bob");
pump(&mut [&mut b], "the team link refused", |s| refused(s[0]));
drop(b);
drop(server);
assert!(!std::fs::read_to_string(dir.path().join("files").join(&file).join("links.json")).unwrap().contains(r#""team""#));
// On again with the old access: the old links stay refused; the next join gets a new one.
let server = crate::start(&team(dir.path(), "view")).unwrap();
let mut b = join(&invite(server.addr(), &file, &view), "Bob");
pump(&mut [&mut b], "the old view team link still refused", |s| refused(s[0]));
assert_ne!(team_link(server.addr(), &file).unwrap(), view);
}
#[test]
fn a_team_link_never_handed_out_again_stays_refused_after_the_setting_goes_back() {
let dir = TempDir::new("team-back");
let mut server = crate::start(&team(dir.path(), "edit")).unwrap();
let (a, file) = share(server.addr(), "Homepage");
let edit = team_link(server.addr(), &file).unwrap();
drop(a);
server.stop();
// View only for a while, and nobody asks for the view team link…
drop(crate::start(&team(dir.path(), "view")).unwrap());
// …then edit again: the edit team link of before doesn't come back.
let server = crate::start(&team(dir.path(), "edit")).unwrap();
let mut b = join(&invite(server.addr(), &file, &edit), "Bob");
pump(&mut [&mut b], "the old edit team link refused", |s| refused(s[0]));
assert_ne!(team_link(server.addr(), &file).unwrap(), edit);
}
#[test]
+2 -1
View File
@@ -684,7 +684,8 @@ resuming with its offline edits, and leaving the session going back to snapshot
- **Revoking.** Revoking a link (`Admin revoke`) that leaves the file without a coded link revokes
its team links too, in the same answer (`Handled::revoked` names every link revoked), so whoever
joined with one is disconnected. Admission refuses a team link while the list is off, gives
another access, or the file isn't shared, whatever `links.json` still holds.
another access, or the file isn't shared, whatever `links.json` still holds, and the server
removes such team links when it starts.
*Built* (document sync between apps):
+3 -1
View File
@@ -193,7 +193,9 @@ someone in, so keep it off on a server open to anyone.
`<web_url>#join=<the link, escaped>`, which opens the file in the browser.
- **Stopping.** When the last of a file's links with a code is revoked (Stop sharing does), its
team link is revoked with it and whoever joined with it is disconnected, as with the other
links. Turning the list off, or changing `team_files`, refuses the old team links at once.
links. Turning the list off, or changing `team_files`, refuses the old team links at once, and
the next start removes them for good (setting it back doesn't bring them back; the next join
makes a new one).
- **What the server keeps.** Unlike other links, a team link's token is kept readable in the
file's `links.json`, so the server can hand it out: as private as the server key beside it.
- **Upgrade note.** An app older than the team list shows no list and can't open its links (it