~ 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 a session's grant leaves out is not asked again for an hour

Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:serve/space-reader-retry
crates/knotless-serve/src/spaces.rs+57−3
@@ -36,9 +36,9 @@ use knotless::url::RepoUrl;
use knotless::xrpc::{Xrpc, XrpcError};
use serde::Deserialize;
use serde_json::{Value, json};
use std::collections::{BTreeSet, HashSet};
use std::sync::Arc;
use std::collections::{BTreeSet, HashMap, HashSet};
use std::sync::atomic::Ordering;
use std::sync::{Arc, LazyLock, Mutex};
use std::time::{Duration, Instant};
/// The collections a private repository's pages read, as the appview
@@ -288,8 +288,10 @@ async fn owned(app: &App, index: &Index, did: &str) -> Result<()> {
// Asked afresh, as a reading is, since a kept credential outlives a
// removal.
space::forget(&ctx);
for uri in index.maybe_member(did)? {
let members = index.maybe_member(did)?.into_iter();
for uri in members.filter(|uri| !out_of_scope(did, uri)) {
let joined = space::joined(&ctx, &uri).await;
refused_for_scope(did, &uri, &joined);
// Anything but an answer leaves it as it was, to ask again.
if joined.is_ok() {
index.read_by(&uri, did)?;
@@ -310,6 +312,37 @@ async fn owned(app: &App, index: &Index, did: &str) -> Result<()> {
Ok(())
}
/// 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`].
static OUT_OF_SCOPE: LazyLock<Mutex<HashMap<(String, String), Instant>>> =
LazyLock::new(Mutex::default);
/// How long a space a session was refused for want of scope is not asked.
const SCOPE_ASKED: Duration = Duration::from_secs(60 * 60);
/// Whether `did`'s session was refused the space `uri` for want of scope
/// within [`SCOPE_ASKED`].
fn out_of_scope(did: &str, uri: &str) -> bool {
let refused = OUT_OF_SCOPE.lock().unwrap();
let at = refused.get(&(did.to_string(), uri.to_string()));
at.is_some_and(|at| at.elapsed() < SCOPE_ASKED)
}
/// Keeps that `did`'s session was refused the space `uri` for want of
/// scope, if `joined` says it was.
fn refused_for_scope<T>(did: &str, uri: &str, joined: &Result<T>) {
if joined
.as_ref()
.err()
.is_some_and(knotless::oauth::out_of_scope)
{
let key = (did.to_string(), uri.to_string());
OUT_OF_SCOPE.lock().unwrap().insert(key, Instant::now());
}
}
/// 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> {
@@ -424,3 +457,24 @@ async fn whole(index: &Index, uri: &str, theirs: &Remote, kem: &PublicKey) -> Re
index.set_space_writer(uri, did, &whole.commit.rev, &whole.hash.state())?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_space_out_of_scope_is_not_asked_again_a_while() {
let (did, uri) = ("did:plc:s", "at://did:plc:o/space/dev.knotless.org/self");
refused_for_scope::<()>(did, uri, &Ok(()));
refused_for_scope::<()>(did, uri, &Err(anyhow::anyhow!("connection refused")));
assert!(!out_of_scope(did, uri));
let scope = anyhow::anyhow!("InvalidToken (403): Scope does not permit reading {uri}");
refused_for_scope::<()>(did, uri, &Err(scope));
assert!(out_of_scope(did, uri));
assert!(!out_of_scope("did:plc:t", uri));
let key = (did.to_string(), uri.to_string());
let long_ago = Instant::now() - SCOPE_ASKED;
OUT_OF_SCOPE.lock().unwrap().insert(key, long_ago);
assert!(!out_of_scope(did, uri));
}
}
crates/knotless-serve/src/telemetry.rs+7−2
@@ -319,8 +319,9 @@ pub fn space_unheard<T>(uri: &str, asked: anyhow::Result<T>) {
/// Logs why `did` was not proven to read the space `uri`, if they were
/// not: at info when its authority refused them, which takes it out of
/// what they are listed as reading; at warn when it could not be asked,
/// which leaves that as it was, to ask again.
/// what they are listed as reading, or when their session's grant leaves
/// the space out, which only logging in again changes; at warn when it
/// could not be asked, which leaves that as it was, to ask again.
pub fn reader_checked<T>(uri: &str, did: &str, joined: &anyhow::Result<T>) {
if let Err(err) = joined {
let fields = Fields {
@@ -331,6 +332,8 @@ pub fn reader_checked<T>(uri: &str, did: &str, joined: &anyhow::Result<T>) {
};
if knotless::space::denied(err) {
telemetry::note(Level::INFO, "space reader refused", &fields);
} else if knotless::oauth::out_of_scope(err) {
telemetry::note(Level::INFO, "space reader out of scope", &fields);
} else {
telemetry::note(Level::WARN, "space reader not checked", &fields);
}
@@ -438,6 +441,8 @@ mod tests {
"did:plc:m",
&Err(anyhow::anyhow!("connection refused")),
);
let scope = anyhow::anyhow!("InvalidToken (403): Scope does not permit reading {uri}");
reader_checked::<()>(uri, "did:plc:m", &Err(scope));
}
#[test]
docs/design/access.md+4−1
@@ -323,7 +323,10 @@ repository on the pages is a repository in that index.
open it, and no longer once its authority refuses them; asking it, as
them, can fail without an answer (the authority unreached, or their
login not let ask), which leaves it listed, to ask again, and logs a
warning, `space reader not checked`. One shared only to
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
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