use std::collections::BTreeMap;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use notedthat_core::{Verb, Who};
use tower::ServiceExt;
use super::fixture::{TOKEN, app, grant, grant_under, json, policy, signed_in_everything};
fn only_notes(
rules: impl IntoIterator<Item = notedthat_core::AccessRule>,
) -> BTreeMap<String, notedthat_core::AccessPolicy> {
BTreeMap::from([("notes".to_string(), policy(rules))])
}
#[tokio::test]
async fn a_knowledge_base_is_listed_when_the_caller_holds_any_grant_in_it() {
let app = app(only_notes([grant(Who::Anyone, [Verb::Read])])).await;
let anonymous = app
.clone()
.oneshot(
Request::builder()
.uri("/api/v1/knowledgebases")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
let authenticated = app
.oneshot(
Request::builder()
.uri("/api/v1/knowledgebases")
.header("authorization", format!("Bearer {TOKEN}"))
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(anonymous.status(), StatusCode::OK);
assert_eq!(
json(anonymous).await["knowledgebases"],
serde_json::json!(["notes"]),
"a knowledge base with no anonymous grant must not be named"
);
assert_eq!(authenticated.status(), StatusCode::OK);
assert_eq!(
json(authenticated).await["knowledgebases"],
serde_json::json!(["notes", "private"]),
"the credential holder can always find a base to repair its manifest"
);
}
#[tokio::test]
async fn a_read_grant_serves_get_and_head_but_never_the_internal_namespace() {
let app = app(only_notes([grant(Who::Anyone, [Verb::Read])])).await;
let get = app
.clone()
.oneshot(
Request::builder()
.uri("/api/v1/knowledgebases/notes/public.md")
.header("range", "bytes=0-5")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
let head = app
.clone()
.oneshot(
Request::builder()
.method("HEAD")
.uri("/api/v1/knowledgebases/notes/public.md")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
let internal = app
.oneshot(
Request::builder()
.uri("/api/v1/knowledgebases/notes/.notedthat%2Fmanifest.json")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(get.status(), StatusCode::PARTIAL_CONTENT);
assert_eq!(head.status(), StatusCode::OK);
assert_eq!(internal.status(), StatusCode::NOT_FOUND);
assert_eq!(json(internal).await["error"], "not_found");
}
#[tokio::test]
async fn a_prefix_scoped_read_grant_serves_only_keys_under_that_prefix() {
let app = app(only_notes([grant_under(
Who::Anyone,
[Verb::Read],
&["public/**"],
)]))
.await;
let inside = app
.clone()
.oneshot(
Request::builder()
.uri("/api/v1/knowledgebases/notes/public%2Fdeep%2Fnote.md")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
let outside = app
.oneshot(
Request::builder()
.uri("/api/v1/knowledgebases/notes/internal%2Fsecret.md")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(inside.status(), StatusCode::OK);
assert_eq!(outside.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn a_list_grant_does_not_imply_a_read_grant() {
let app = app(only_notes([grant(Who::Anyone, [Verb::List])])).await;
let listing = app
.clone()
.oneshot(
Request::builder()
.uri("/api/v1/knowledgebases/notes")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
let read = app
.oneshot(
Request::builder()
.uri("/api/v1/knowledgebases/notes/public.md")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(listing.status(), StatusCode::OK);
assert_eq!(read.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn a_restricted_credential_is_refused_with_forbidden_rather_than_unauthorized() {
let app = app(only_notes([grant_under(
Who::SignedIn,
[Verb::Read],
&["public/**"],
)]))
.await;
let granted = app
.clone()
.oneshot(
Request::builder()
.uri("/api/v1/knowledgebases/notes/public%2Findex.md")
.header("authorization", format!("Bearer {TOKEN}"))
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
let denied = app
.oneshot(
Request::builder()
.uri("/api/v1/knowledgebases/notes/internal%2Fsecret.md")
.header("authorization", format!("Bearer {TOKEN}"))
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(granted.status(), StatusCode::OK);
assert_eq!(denied.status(), StatusCode::FORBIDDEN);
assert_eq!(json(denied).await["error"], "forbidden");
}
#[tokio::test]
async fn the_credential_holder_can_always_rewrite_the_manifest_that_locked_it_out() {
let app = app(only_notes([])).await;
let read_manifest = app
.clone()
.oneshot(
Request::builder()
.uri("/api/v1/knowledgebases/notes/.notedthat%2Fmanifest.json")
.header("authorization", format!("Bearer {TOKEN}"))
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
let write_manifest = app
.clone()
.oneshot(
Request::builder()
.method("PUT")
.uri("/api/v1/knowledgebases/notes/.notedthat%2Fmanifest.json")
.header("authorization", format!("Bearer {TOKEN}"))
.header("content-type", "application/json")
.body(Body::from("{}"))
.expect("request"),
)
.await
.expect("response");
let ordinary_object = app
.oneshot(
Request::builder()
.uri("/api/v1/knowledgebases/notes/public.md")
.header("authorization", format!("Bearer {TOKEN}"))
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(read_manifest.status(), StatusCode::OK);
assert!(
write_manifest.status().is_success(),
"repair must be possible through the API, not only through the bucket"
);
assert_eq!(
ordinary_object.status(),
StatusCode::FORBIDDEN,
"the valve is the internal namespace only; everything else stays denied"
);
}
#[tokio::test]
async fn an_anonymous_caller_never_reaches_the_internal_namespace_under_any_grant() {
let app = app(only_notes([
grant(Who::Anyone, [Verb::List, Verb::Read, Verb::Search]),
signed_in_everything(),
]))
.await;
let read = app
.clone()
.oneshot(
Request::builder()
.uri("/api/v1/knowledgebases/notes/.notedthat%2Fmanifest.json")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
let listing = app
.oneshot(
Request::builder()
.uri("/api/v1/knowledgebases/notes")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(read.status(), StatusCode::NOT_FOUND);
let keys = super::fixture::listed_keys(listing).await;
assert!(
!keys.iter().any(|key| key.starts_with(".notedthat")),
"the internal namespace must not appear in an anonymous listing: {keys:?}"
);
}