~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/knotless

git that lives in your Atmosphere account

knotless.dev
1 collaborator

knotless 0.13.0 Latest 2 packages, 1 image

1
10

A space's syncer refused for want of scope is not asked again at every sweep

Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:serve/syncer-scope
crates/knotless-serve/src/gateway.rs+3−1
@@ -425,7 +425,9 @@ pub async fn callback(
Duration::ZERO,
Some(&session.did),
);
session::save(&app.ctx, &session)
let saved = session::save(&app.ctx, &session);
crate::spaces::in_scope_again(&session.did);
saved
}
Err(err) => Err(err),
};
crates/knotless-serve/src/login.rs+1−0
@@ -605,6 +605,7 @@ async fn keep(
let key = oauth::client_key(&app.ctx)?;
oauth::set_client_key(&ctx, &key)?;
session::save(&ctx, &session)?;
crate::spaces::in_scope_again(&session.did);
Ok(session.did)
}
crates/knotless-serve/src/spaces.rs+47−4
@@ -92,8 +92,7 @@ pub async fn follow(app: Arc<App>, every: Duration) {
swept = Some(Instant::now());
}
let due = std::mem::take(&mut *app.due.lock().unwrap());
let syncers = app.syncers().into_iter();
for (uri, did) in syncers.filter(|(uri, _)| all || due.contains(uri)) {
for (uri, did) in to_sync(app.syncers(), all, &due) {
let ctx = crate::login::syncer_ctx(&app, &did);
// A credential asked for afresh now and then, which the owner's
// PDS gives only to one still a member.
@@ -101,6 +100,7 @@ pub async fn follow(app: Arc<App>, every: Duration) {
space::forget(&ctx);
}
let synced = sync(&ctx, &index, &uri).await;
refused_for_scope(&did, &uri, &synced);
let failed = synced.is_err();
if !failed {
app.synced_now(&uri);
@@ -135,6 +135,22 @@ pub async fn follow(app: Arc<App>, every: Duration) {
}
}
/// Of `syncers`, each a space and the member it is synced as, those to
/// sync now: every one on a sweep (`all`), else those whose authority told
/// of a write there (`due`). None whose member was refused it for want of
/// scope lately ([`out_of_scope`]): their grant stays as it was until they
/// log in again, and the space's readers sync it meanwhile, once it goes
/// [`STALE`].
fn to_sync(
syncers: Vec<(String, String)>,
all: bool,
due: &HashSet<String>,
) -> Vec<(String, String)> {
let now = |(uri, _): &(String, String)| all || due.contains(uri);
let asked = |(uri, did): &(String, String)| !out_of_scope(did, uri);
syncers.into_iter().filter(now).filter(asked).collect()
}
/// Of [`space::denied`], what says the space itself is no more, rather
/// than that its syncer may no longer read it.
fn gone(err: &anyhow::Error) -> bool {
@@ -334,8 +350,9 @@ async fn owned(app: &App, index: &Index, did: &str) -> Result<()> {
/// The spaces each account's session was refused for want of scope, by
/// account and space, and when: not asked again until [`SCOPE_ASKED`]
/// passes, as its grant stays as it was until it logs in again, and each
/// asking costs a call to its PDS every [`DISCOVER`].
/// passes, or it logs in again ([`in_scope_again`]), as its grant stays as
/// it was until then, and each asking costs a call to its PDS, every
/// [`DISCOVER`] as a reader, every sweep of [`follow`] as a syncer.
static OUT_OF_SCOPE: LazyLock<Mutex<HashMap<(String, String), Instant>>> =
LazyLock::new(Mutex::default);
@@ -363,6 +380,12 @@ fn refused_for_scope<T>(did: &str, uri: &str, joined: &Result<T>) {
}
}
/// Forgets the spaces `did`'s session was refused for want of scope: it
/// was granted anew, as by logging in again, so each is asked again.
pub fn in_scope_again(did: &str) {
OUT_OF_SCOPE.lock().unwrap().retain(|(of, _), _| of != did);
}
/// Takes in what was written to the space `uri` since it was last synced,
/// and says whether anything was.
pub async fn sync(ctx: &Ctx, index: &Index, uri: &str) -> Result<bool> {
@@ -560,4 +583,24 @@ mod tests {
OUT_OF_SCOPE.lock().unwrap().insert(key, long_ago);
assert!(!out_of_scope(did, uri));
}
#[test]
fn syncers_out_of_scope_wait_till_they_log_in_again() {
let did = "did:plc:syncer";
let (uri, other) = (
"at://did:plc:o/space/dev.knotless.space/r",
"at://did:plc:o/space/dev.knotless.space/s",
);
let syncers = || vec![(uri.into(), did.into()), (other.into(), did.into())];
let uris = |s: Vec<(String, String)>| s.into_iter().map(|(u, _)| u).collect::<Vec<_>>();
let none = HashSet::new();
assert_eq!(uris(to_sync(syncers(), true, &none)), [uri, other]);
let due = HashSet::from([other.to_string()]);
assert_eq!(uris(to_sync(syncers(), false, &due)), [other]);
let scope = anyhow::anyhow!("InvalidToken (403): Scope does not permit reading {uri}");
refused_for_scope::<()>(did, uri, &Err(scope));
assert_eq!(uris(to_sync(syncers(), true, &none)), [other]);
in_scope_again(did);
assert_eq!(uris(to_sync(syncers(), true, &none)), [uri, other]);
}
}
docs/design/access.md+6−3
@@ -306,7 +306,10 @@ repository on the pages is a repository in that index.
makes every reader wait. Its syncer's sync can fail without its
being refused, as when their session here lapses or its grant leaves
the space out; until they logged in again, every write since would
otherwise be missing from it, for every reader. A reader's sync that
otherwise be missing from it, for every reader. A syncer whose grant
leaves the space out is not asked to sync it again for an hour, or
until they log in or delegate again, rather than at every sweep: its
readers sync it meanwhile. A reader's sync that
fails is logged, `space not synced`, and the repository is answered from
the index as it was, then tried again a minute on; only one never synced
fails the read.
@@ -344,8 +347,8 @@ repository on the pages is a repository in that index.
login not let ask), which leaves it listed, to ask again, and logs a
warning, `space reader not checked`. A login whose grant leaves the
space out logs `space reader out of scope`, at info, and that space is
not asked as them again for an hour, as their grant stays as it is
until they log in again. One shared only to
not asked as them again for an hour, or until they log in again, as
their grant stays as it is until then. One shared only to
read names them only in `readers`, which the server takes for
notifications alone, so is listed to them once they have read it here. Which they
were is kept when they log out, listed to no one meanwhile, and asked