use std::collections::BTreeMap;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use notedthat_core::{AccessPolicy, Verb, Who};
use tower::ServiceExt;
use super::fixture::{TOKEN, app, grant, grant_under, json, policy, signed_in_everything};
fn notes(
rules: impl IntoIterator<Item = notedthat_core::AccessRule>,
) -> BTreeMap<String, AccessPolicy> {
BTreeMap::from([("notes".to_string(), policy(rules))])
}
const MUTATIONS: &[(&str, &str)] = &[
("PUT", "/api/v1/knowledgebases/notes/new.md"),
("PATCH", "/api/v1/knowledgebases/notes/new.md"),
("DELETE", "/api/v1/knowledgebases/notes/new.md"),
("POST", "/api/v1/knowledgebases/notes/replace/new.md"),
];
#[tokio::test]
async fn a_supplied_credential_that_does_not_verify_is_never_treated_as_anonymous() {
let app = app(notes([grant(Who::Anyone, [Verb::Read])])).await;
for authorization in ["Basic abc", "Bearer wrong", "Bearer "] {
let response = app
.clone()
.oneshot(
Request::builder()
.uri("/api/v1/knowledgebases/notes/public.md")
.header("authorization", authorization)
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(
response.status(),
StatusCode::UNAUTHORIZED,
"{authorization}"
);
assert_eq!(json(response).await["error"], "unauthorized");
}
let mut non_utf8 = Request::builder()
.uri("/api/v1/knowledgebases/notes/public.md")
.body(Body::empty())
.expect("request");
non_utf8.headers_mut().insert(
axum::http::header::AUTHORIZATION,
axum::http::HeaderValue::from_bytes(&[0xff]).expect("opaque header value"),
);
let non_utf8_response = app.clone().oneshot(non_utf8).await.expect("response");
let mut duplicated = Request::builder()
.uri("/api/v1/knowledgebases/notes/public.md")
.header("authorization", format!("Bearer {TOKEN}"))
.body(Body::empty())
.expect("request");
duplicated.headers_mut().append(
axum::http::header::AUTHORIZATION,
axum::http::HeaderValue::from_static("Bearer wrong"),
);
let duplicated_response = app.oneshot(duplicated).await.expect("response");
assert_eq!(non_utf8_response.status(), StatusCode::UNAUTHORIZED);
assert_eq!(duplicated_response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn anonymous_mutation_is_refused_under_the_broadest_grant_and_changes_nothing() {
let app = app(notes([
grant(Who::Anyone, [Verb::List, Verb::Read, Verb::Search]),
signed_in_everything(),
]))
.await;
for (method, uri) in MUTATIONS {
let write = app
.clone()
.oneshot(
Request::builder()
.method(*method)
.uri(*uri)
.header("if-match", "\"abc\"")
.body(Body::from("new body"))
.expect("request"),
)
.await
.expect("response");
assert_eq!(write.status(), StatusCode::UNAUTHORIZED, "{method} {uri}");
}
let read = app
.oneshot(
Request::builder()
.uri("/api/v1/knowledgebases/notes/new.md")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(read.status(), StatusCode::NOT_FOUND, "nothing was written");
}
#[tokio::test]
async fn a_write_grant_does_not_carry_a_delete_grant() {
let app = app(notes([grant(
Who::SignedIn,
[Verb::List, Verb::Read, Verb::Write],
)]))
.await;
let write = app
.clone()
.oneshot(
Request::builder()
.method("PUT")
.uri("/api/v1/knowledgebases/notes/new.md")
.header("authorization", format!("Bearer {TOKEN}"))
.body(Body::from("new body"))
.expect("request"),
)
.await
.expect("response");
let delete = app
.oneshot(
Request::builder()
.method("DELETE")
.uri("/api/v1/knowledgebases/notes/new.md")
.header("authorization", format!("Bearer {TOKEN}"))
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert!(write.status().is_success(), "{:?}", write.status());
assert_eq!(delete.status(), StatusCode::FORBIDDEN);
}
#[tokio::test]
async fn a_prefix_scoped_write_grant_stops_at_its_prefix() {
let app = app(notes([grant_under(
Who::SignedIn,
[Verb::Write],
&["public/**"],
)]))
.await;
let inside = app
.clone()
.oneshot(
Request::builder()
.method("PUT")
.uri("/api/v1/knowledgebases/notes/public%2Fnew.md")
.header("authorization", format!("Bearer {TOKEN}"))
.body(Body::from("new body"))
.expect("request"),
)
.await
.expect("response");
let outside = app
.oneshot(
Request::builder()
.method("PUT")
.uri("/api/v1/knowledgebases/notes/internal%2Fnew.md")
.header("authorization", format!("Bearer {TOKEN}"))
.body(Body::from("new body"))
.expect("request"),
)
.await
.expect("response");
assert!(inside.status().is_success(), "{:?}", inside.status());
assert_eq!(outside.status(), StatusCode::FORBIDDEN);
}