mirror of
https://github.com/thegeneralist01/archivr
synced 2026-07-21 18:55:36 +02:00
feat(server): enforce auth on all public read endpoints
Add auth_user: AuthUser + require_auth() to entry_detail, list_runs, serve_artifact, serve_entry_favicon, serve_blob, list_tags, list_entry_tags, list_collections_handler, list_entry_collections_handler. Add require_auth() to get_collection_handler (already had extractor). Update existing tests to pass session cookies.
This commit is contained in:
parent
a8e52bdf71
commit
007c5fc5fb
1 changed files with 355 additions and 36 deletions
|
|
@ -311,8 +311,10 @@ async fn search_entries_handler(
|
||||||
|
|
||||||
async fn entry_detail(
|
async fn entry_detail(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
auth_user: AuthUser,
|
||||||
Path((archive_id, entry_uid)): Path<(String, String)>,
|
Path((archive_id, entry_uid)): Path<(String, String)>,
|
||||||
) -> Result<Json<archive::EntryDetail>, ApiError> {
|
) -> Result<Json<archive::EntryDetail>, ApiError> {
|
||||||
|
auth_user.require_auth()?;
|
||||||
let mounted = mounted_archive(&state, &archive_id)?;
|
let mounted = mounted_archive(&state, &archive_id)?;
|
||||||
let conn = database::open_or_initialize(&mounted.archive_path)?;
|
let conn = database::open_or_initialize(&mounted.archive_path)?;
|
||||||
let detail = archive::get_entry_detail(&conn, &entry_uid)?
|
let detail = archive::get_entry_detail(&conn, &entry_uid)?
|
||||||
|
|
@ -322,8 +324,10 @@ async fn entry_detail(
|
||||||
|
|
||||||
async fn list_runs(
|
async fn list_runs(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
auth_user: AuthUser,
|
||||||
Path(archive_id): Path<String>,
|
Path(archive_id): Path<String>,
|
||||||
) -> Result<Json<Vec<archive::RunSummary>>, ApiError> {
|
) -> Result<Json<Vec<archive::RunSummary>>, ApiError> {
|
||||||
|
auth_user.require_auth()?;
|
||||||
let mounted = mounted_archive(&state, &archive_id)?;
|
let mounted = mounted_archive(&state, &archive_id)?;
|
||||||
let conn = database::open_or_initialize(&mounted.archive_path)?;
|
let conn = database::open_or_initialize(&mounted.archive_path)?;
|
||||||
Ok(Json(archive::list_runs(&conn)?))
|
Ok(Json(archive::list_runs(&conn)?))
|
||||||
|
|
@ -331,9 +335,11 @@ async fn list_runs(
|
||||||
|
|
||||||
async fn serve_artifact(
|
async fn serve_artifact(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
auth_user: AuthUser,
|
||||||
Path((archive_id, entry_uid, artifact_index)): Path<(String, String, usize)>,
|
Path((archive_id, entry_uid, artifact_index)): Path<(String, String, usize)>,
|
||||||
req: Request,
|
req: Request,
|
||||||
) -> Result<Response, ApiError> {
|
) -> Result<Response, ApiError> {
|
||||||
|
auth_user.require_auth()?;
|
||||||
let mounted = mounted_archive(&state, &archive_id)?;
|
let mounted = mounted_archive(&state, &archive_id)?;
|
||||||
let paths = archive::read_archive_paths(&mounted.archive_path)?;
|
let paths = archive::read_archive_paths(&mounted.archive_path)?;
|
||||||
let conn = database::open_or_initialize(&mounted.archive_path)?;
|
let conn = database::open_or_initialize(&mounted.archive_path)?;
|
||||||
|
|
@ -344,8 +350,6 @@ async fn serve_artifact(
|
||||||
.get(artifact_index)
|
.get(artifact_index)
|
||||||
.ok_or(ApiError::not_found("artifact index out of range"))?;
|
.ok_or(ApiError::not_found("artifact index out of range"))?;
|
||||||
let file_path = archive::resolve_artifact_path(&paths.store_path, artifact)?;
|
let file_path = archive::resolve_artifact_path(&paths.store_path, artifact)?;
|
||||||
// ServeFile streams the file, handles Range requests (video seeking),
|
|
||||||
// sets Content-Type/ETag/Last-Modified. Error type is Infallible.
|
|
||||||
Ok(ServeFile::new(&file_path)
|
Ok(ServeFile::new(&file_path)
|
||||||
.oneshot(req)
|
.oneshot(req)
|
||||||
.await
|
.await
|
||||||
|
|
@ -355,9 +359,11 @@ async fn serve_artifact(
|
||||||
|
|
||||||
async fn serve_entry_favicon(
|
async fn serve_entry_favicon(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
auth_user: AuthUser,
|
||||||
Path((archive_id, entry_uid)): Path<(String, String)>,
|
Path((archive_id, entry_uid)): Path<(String, String)>,
|
||||||
req: Request,
|
req: Request,
|
||||||
) -> Result<Response, ApiError> {
|
) -> Result<Response, ApiError> {
|
||||||
|
auth_user.require_auth()?;
|
||||||
let mounted = mounted_archive(&state, &archive_id)?;
|
let mounted = mounted_archive(&state, &archive_id)?;
|
||||||
let paths = archive::read_archive_paths(&mounted.archive_path)?;
|
let paths = archive::read_archive_paths(&mounted.archive_path)?;
|
||||||
let conn = database::open_or_initialize(&mounted.archive_path)?;
|
let conn = database::open_or_initialize(&mounted.archive_path)?;
|
||||||
|
|
@ -378,19 +384,17 @@ async fn serve_entry_favicon(
|
||||||
|
|
||||||
async fn serve_blob(
|
async fn serve_blob(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
auth_user: AuthUser,
|
||||||
Path((archive_id, sha256)): Path<(String, String)>,
|
Path((archive_id, sha256)): Path<(String, String)>,
|
||||||
req: Request,
|
req: Request,
|
||||||
) -> Result<Response, ApiError> {
|
) -> Result<Response, ApiError> {
|
||||||
|
auth_user.require_auth()?;
|
||||||
let mounted = mounted_archive(&state, &archive_id)?;
|
let mounted = mounted_archive(&state, &archive_id)?;
|
||||||
let paths = archive::read_archive_paths(&mounted.archive_path)?;
|
let paths = archive::read_archive_paths(&mounted.archive_path)?;
|
||||||
let conn = database::open_or_initialize(&mounted.archive_path)?;
|
let conn = database::open_or_initialize(&mounted.archive_path)?;
|
||||||
|
|
||||||
let blob = database::get_blob_by_sha256(&conn, &sha256)?
|
let blob = database::get_blob_by_sha256(&conn, &sha256)?
|
||||||
.ok_or(ApiError::not_found("blob not found"))?;
|
.ok_or(ApiError::not_found("blob not found"))?;
|
||||||
|
|
||||||
let file_path = paths.store_path.join(&blob.raw_relpath);
|
let file_path = paths.store_path.join(&blob.raw_relpath);
|
||||||
|
|
||||||
// Path-traversal guard: resolved path must stay inside store_path.
|
|
||||||
let canonical_file = file_path
|
let canonical_file = file_path
|
||||||
.canonicalize()
|
.canonicalize()
|
||||||
.map_err(|_| ApiError::not_found("blob file not found"))?;
|
.map_err(|_| ApiError::not_found("blob file not found"))?;
|
||||||
|
|
@ -401,7 +405,6 @@ async fn serve_blob(
|
||||||
if !canonical_file.starts_with(&canonical_store) {
|
if !canonical_file.starts_with(&canonical_store) {
|
||||||
return Err(ApiError::not_found("blob not found"));
|
return Err(ApiError::not_found("blob not found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ServeFile::new(&canonical_file)
|
Ok(ServeFile::new(&canonical_file)
|
||||||
.oneshot(req)
|
.oneshot(req)
|
||||||
.await
|
.await
|
||||||
|
|
@ -449,8 +452,10 @@ struct PatchCollectionBody {
|
||||||
|
|
||||||
async fn list_tags(
|
async fn list_tags(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
auth_user: AuthUser,
|
||||||
Path(archive_id): Path<String>,
|
Path(archive_id): Path<String>,
|
||||||
) -> Result<Json<Vec<archive::TagNode>>, ApiError> {
|
) -> Result<Json<Vec<archive::TagNode>>, ApiError> {
|
||||||
|
auth_user.require_auth()?;
|
||||||
let mounted = mounted_archive(&state, &archive_id)?;
|
let mounted = mounted_archive(&state, &archive_id)?;
|
||||||
let conn = database::open_or_initialize(&mounted.archive_path)?;
|
let conn = database::open_or_initialize(&mounted.archive_path)?;
|
||||||
Ok(Json(archive::list_tag_tree(&conn)?))
|
Ok(Json(archive::list_tag_tree(&conn)?))
|
||||||
|
|
@ -474,8 +479,10 @@ async fn create_tag_handler(
|
||||||
|
|
||||||
async fn list_entry_tags(
|
async fn list_entry_tags(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
auth_user: AuthUser,
|
||||||
Path((archive_id, entry_uid)): Path<(String, String)>,
|
Path((archive_id, entry_uid)): Path<(String, String)>,
|
||||||
) -> Result<Json<Vec<archive::Tag>>, ApiError> {
|
) -> Result<Json<Vec<archive::Tag>>, ApiError> {
|
||||||
|
auth_user.require_auth()?;
|
||||||
let mounted = mounted_archive(&state, &archive_id)?;
|
let mounted = mounted_archive(&state, &archive_id)?;
|
||||||
let conn = database::open_or_initialize(&mounted.archive_path)?;
|
let conn = database::open_or_initialize(&mounted.archive_path)?;
|
||||||
match archive::get_entry_tags(&conn, &entry_uid)? {
|
match archive::get_entry_tags(&conn, &entry_uid)? {
|
||||||
|
|
@ -1022,8 +1029,10 @@ impl IntoResponse for ApiError {
|
||||||
|
|
||||||
async fn list_collections_handler(
|
async fn list_collections_handler(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
auth_user: AuthUser,
|
||||||
Path(archive_id): Path<String>,
|
Path(archive_id): Path<String>,
|
||||||
) -> Result<Json<Vec<archive::CollectionSummary>>, ApiError> {
|
) -> Result<Json<Vec<archive::CollectionSummary>>, ApiError> {
|
||||||
|
auth_user.require_auth()?;
|
||||||
let mounted = mounted_archive(&state, &archive_id)?;
|
let mounted = mounted_archive(&state, &archive_id)?;
|
||||||
let conn = database::open_or_initialize(&mounted.archive_path)?;
|
let conn = database::open_or_initialize(&mounted.archive_path)?;
|
||||||
Ok(Json(archive::list_collections(&conn)?))
|
Ok(Json(archive::list_collections(&conn)?))
|
||||||
|
|
@ -1060,6 +1069,7 @@ async fn get_collection_handler(
|
||||||
auth: AuthUser,
|
auth: AuthUser,
|
||||||
Path((archive_id, coll_uid)): Path<(String, String)>,
|
Path((archive_id, coll_uid)): Path<(String, String)>,
|
||||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||||
|
auth.require_auth()?;
|
||||||
let mounted = mounted_archive(&state, &archive_id)?;
|
let mounted = mounted_archive(&state, &archive_id)?;
|
||||||
let conn = database::open_or_initialize(&mounted.archive_path)?;
|
let conn = database::open_or_initialize(&mounted.archive_path)?;
|
||||||
let record = database::get_collection_by_uid(&conn, &coll_uid)?
|
let record = database::get_collection_by_uid(&conn, &coll_uid)?
|
||||||
|
|
@ -1186,8 +1196,10 @@ async fn update_entry_visibility_handler(
|
||||||
|
|
||||||
async fn list_entry_collections_handler(
|
async fn list_entry_collections_handler(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
auth_user: AuthUser,
|
||||||
Path((archive_id, entry_uid)): Path<(String, String)>,
|
Path((archive_id, entry_uid)): Path<(String, String)>,
|
||||||
) -> Result<Json<Vec<archive::EntryCollectionMembership>>, ApiError> {
|
) -> Result<Json<Vec<archive::EntryCollectionMembership>>, ApiError> {
|
||||||
|
auth_user.require_auth()?;
|
||||||
let mounted = mounted_archive(&state, &archive_id)?;
|
let mounted = mounted_archive(&state, &archive_id)?;
|
||||||
let conn = database::open_or_initialize(&mounted.archive_path)?;
|
let conn = database::open_or_initialize(&mounted.archive_path)?;
|
||||||
match archive::get_entry_collections(&conn, &entry_uid)? {
|
match archive::get_entry_collections(&conn, &entry_uid)? {
|
||||||
|
|
@ -1383,16 +1395,14 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn artifact_missing_archive_returns_404() {
|
async fn artifact_missing_archive_returns_404() {
|
||||||
let (test_app, _dir) = make_test_app();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let response = test_app
|
let auth_path = dir.path().join("auth.sqlite");
|
||||||
.oneshot(
|
{ let conn = archivr_core::database::open_auth_db(&auth_path).unwrap(); archivr_core::database::create_owner(&conn, "testowner", "dummy").unwrap(); }
|
||||||
Request::builder()
|
let session_cookie = make_test_session(&auth_path);
|
||||||
.uri("/api/archives/nope/entries/entry_abc/artifacts/0")
|
let registry = ServerRegistry { archives: vec![], bind: None, auth_db_path: None };
|
||||||
.body(Body::empty())
|
let response = app(registry, auth_path)
|
||||||
.unwrap(),
|
.oneshot(Request::builder().uri("/api/archives/nope/entries/entry_abc/artifacts/0").header("cookie", &session_cookie).body(Body::empty()).unwrap())
|
||||||
)
|
.await.unwrap();
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1412,6 +1422,7 @@ mod tests {
|
||||||
let conn = archivr_core::database::open_auth_db(&auth_path).unwrap();
|
let conn = archivr_core::database::open_auth_db(&auth_path).unwrap();
|
||||||
archivr_core::database::create_owner(&conn, "testowner", "dummy").unwrap();
|
archivr_core::database::create_owner(&conn, "testowner", "dummy").unwrap();
|
||||||
}
|
}
|
||||||
|
let session_cookie = make_test_session(&auth_path);
|
||||||
let registry = ServerRegistry {
|
let registry = ServerRegistry {
|
||||||
archives: vec![MountedArchive {
|
archives: vec![MountedArchive {
|
||||||
id: "test".to_string(),
|
id: "test".to_string(),
|
||||||
|
|
@ -1425,6 +1436,7 @@ mod tests {
|
||||||
.oneshot(
|
.oneshot(
|
||||||
Request::builder()
|
Request::builder()
|
||||||
.uri("/api/archives/test/entries/entry_doesnotexist/artifacts/0")
|
.uri("/api/archives/test/entries/entry_doesnotexist/artifacts/0")
|
||||||
|
.header("cookie", &session_cookie)
|
||||||
.body(Body::empty())
|
.body(Body::empty())
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
|
|
@ -1449,6 +1461,7 @@ mod tests {
|
||||||
let conn = archivr_core::database::open_auth_db(&auth_path).unwrap();
|
let conn = archivr_core::database::open_auth_db(&auth_path).unwrap();
|
||||||
archivr_core::database::create_owner(&conn, "testowner", "dummy").unwrap();
|
archivr_core::database::create_owner(&conn, "testowner", "dummy").unwrap();
|
||||||
}
|
}
|
||||||
|
let session_cookie = make_test_session(&auth_path);
|
||||||
let registry = ServerRegistry {
|
let registry = ServerRegistry {
|
||||||
archives: vec![MountedArchive {
|
archives: vec![MountedArchive {
|
||||||
id: "test".to_string(),
|
id: "test".to_string(),
|
||||||
|
|
@ -1462,6 +1475,7 @@ mod tests {
|
||||||
.oneshot(
|
.oneshot(
|
||||||
Request::builder()
|
Request::builder()
|
||||||
.uri("/api/archives/test/entries/entry_doesnotexist/artifacts/99")
|
.uri("/api/archives/test/entries/entry_doesnotexist/artifacts/99")
|
||||||
|
.header("cookie", &session_cookie)
|
||||||
.body(Body::empty())
|
.body(Body::empty())
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
|
|
@ -1548,6 +1562,7 @@ mod tests {
|
||||||
let conn = archivr_core::database::open_auth_db(&auth_path).unwrap();
|
let conn = archivr_core::database::open_auth_db(&auth_path).unwrap();
|
||||||
archivr_core::database::create_owner(&conn, "testowner", "dummy").unwrap();
|
archivr_core::database::create_owner(&conn, "testowner", "dummy").unwrap();
|
||||||
}
|
}
|
||||||
|
let session_cookie = make_test_session(&auth_path);
|
||||||
let registry = ServerRegistry {
|
let registry = ServerRegistry {
|
||||||
archives: vec![MountedArchive {
|
archives: vec![MountedArchive {
|
||||||
id: "test".to_string(),
|
id: "test".to_string(),
|
||||||
|
|
@ -1562,7 +1577,7 @@ mod tests {
|
||||||
entry.entry_uid
|
entry.entry_uid
|
||||||
);
|
);
|
||||||
let response = app(registry, auth_path)
|
let response = app(registry, auth_path)
|
||||||
.oneshot(Request::builder().uri(&uri).body(Body::empty()).unwrap())
|
.oneshot(Request::builder().uri(&uri).header("cookie", &session_cookie).body(Body::empty()).unwrap())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
|
@ -1619,16 +1634,14 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_list_tags_unknown_archive() {
|
async fn test_list_tags_unknown_archive() {
|
||||||
let (test_app, _dir) = make_test_app();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let response = test_app
|
let auth_path = dir.path().join("auth.sqlite");
|
||||||
.oneshot(
|
{ let conn = archivr_core::database::open_auth_db(&auth_path).unwrap(); archivr_core::database::create_owner(&conn, "testowner", "dummy").unwrap(); }
|
||||||
Request::builder()
|
let session_cookie = make_test_session(&auth_path);
|
||||||
.uri("/api/archives/ghost/tags")
|
let registry = ServerRegistry { archives: vec![], bind: None, auth_db_path: None };
|
||||||
.body(Body::empty())
|
let response = app(registry, auth_path)
|
||||||
.unwrap(),
|
.oneshot(Request::builder().uri("/api/archives/ghost/tags").header("cookie", &session_cookie).body(Body::empty()).unwrap())
|
||||||
)
|
.await.unwrap();
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1690,6 +1703,7 @@ mod tests {
|
||||||
.oneshot(
|
.oneshot(
|
||||||
Request::builder()
|
Request::builder()
|
||||||
.uri("/api/archives/test/tags")
|
.uri("/api/archives/test/tags")
|
||||||
|
.header("cookie", &session_cookie)
|
||||||
.body(Body::empty())
|
.body(Body::empty())
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
|
|
@ -1737,6 +1751,7 @@ mod tests {
|
||||||
.oneshot(
|
.oneshot(
|
||||||
Request::builder()
|
Request::builder()
|
||||||
.uri(&entry_tags_uri)
|
.uri(&entry_tags_uri)
|
||||||
|
.header("cookie", &session_cookie)
|
||||||
.body(Body::empty())
|
.body(Body::empty())
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
|
|
@ -1766,6 +1781,7 @@ mod tests {
|
||||||
.oneshot(
|
.oneshot(
|
||||||
Request::builder()
|
Request::builder()
|
||||||
.uri(&entry_tags_uri)
|
.uri(&entry_tags_uri)
|
||||||
|
.header("cookie", &session_cookie)
|
||||||
.body(Body::empty())
|
.body(Body::empty())
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
|
|
@ -1842,15 +1858,10 @@ mod tests {
|
||||||
async fn test_list_entry_tags_unknown_entry() {
|
async fn test_list_entry_tags_unknown_entry() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let (registry, _, auth_path) = make_test_registry(&dir);
|
let (registry, _, auth_path) = make_test_registry(&dir);
|
||||||
|
let session_cookie = make_test_session(&auth_path);
|
||||||
let response = app(registry, auth_path)
|
let response = app(registry, auth_path)
|
||||||
.oneshot(
|
.oneshot(Request::builder().uri("/api/archives/test/entries/ghost_uid/tags").header("cookie", &session_cookie).body(Body::empty()).unwrap())
|
||||||
Request::builder()
|
.await.unwrap();
|
||||||
.uri("/api/archives/test/entries/ghost_uid/tags")
|
|
||||||
.body(Body::empty())
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1959,6 +1970,7 @@ mod tests {
|
||||||
let conn = archivr_core::database::open_auth_db(&auth_path).unwrap();
|
let conn = archivr_core::database::open_auth_db(&auth_path).unwrap();
|
||||||
archivr_core::database::create_owner(&conn, "testowner", "dummy").unwrap();
|
archivr_core::database::create_owner(&conn, "testowner", "dummy").unwrap();
|
||||||
}
|
}
|
||||||
|
let session_cookie = make_test_session(&auth_path);
|
||||||
let registry = ServerRegistry {
|
let registry = ServerRegistry {
|
||||||
archives: vec![MountedArchive {
|
archives: vec![MountedArchive {
|
||||||
id: "test".to_string(),
|
id: "test".to_string(),
|
||||||
|
|
@ -1972,6 +1984,7 @@ mod tests {
|
||||||
.oneshot(
|
.oneshot(
|
||||||
Request::builder()
|
Request::builder()
|
||||||
.uri("/api/archives/test/blobs/0000000000000000000000000000000000000000000000000000000000000000")
|
.uri("/api/archives/test/blobs/0000000000000000000000000000000000000000000000000000000000000000")
|
||||||
|
.header("cookie", &session_cookie)
|
||||||
.body(Body::empty())
|
.body(Body::empty())
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
|
|
@ -2354,4 +2367,310 @@ mod tests {
|
||||||
assert_eq!(resp.status(), StatusCode::OK, "/health must be unaffected");
|
assert_eq!(resp.status(), StatusCode::OK, "/health must be unaffected");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Task 1: read-endpoint auth enforcement ────────────────────────────────
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn entry_detail_requires_auth() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let (registry, _, auth_path) = make_test_registry(&dir);
|
||||||
|
let response = app(registry, auth_path)
|
||||||
|
.oneshot(Request::builder().uri("/api/archives/test/entries/fake_uid").body(Body::empty()).unwrap())
|
||||||
|
.await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn entry_detail_with_auth_returns_ok() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let (registry, archive_path, auth_path) = make_test_registry(&dir);
|
||||||
|
let entry = make_test_entry(&archive_path);
|
||||||
|
let session_cookie = make_test_session(&auth_path);
|
||||||
|
let uri = format!("/api/archives/test/entries/{}", entry.entry_uid);
|
||||||
|
let response = app(registry, auth_path)
|
||||||
|
.oneshot(Request::builder().uri(&uri).header("cookie", &session_cookie).body(Body::empty()).unwrap())
|
||||||
|
.await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn list_runs_requires_auth() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let (registry, _, auth_path) = make_test_registry(&dir);
|
||||||
|
let response = app(registry, auth_path)
|
||||||
|
.oneshot(Request::builder().uri("/api/archives/test/runs").body(Body::empty()).unwrap())
|
||||||
|
.await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn list_runs_with_auth_returns_ok() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let (registry, _, auth_path) = make_test_registry(&dir);
|
||||||
|
let session_cookie = make_test_session(&auth_path);
|
||||||
|
let response = app(registry, auth_path)
|
||||||
|
.oneshot(Request::builder().uri("/api/archives/test/runs").header("cookie", &session_cookie).body(Body::empty()).unwrap())
|
||||||
|
.await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn serve_artifact_requires_auth() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let (registry, _, auth_path) = make_test_registry(&dir);
|
||||||
|
let response = app(registry, auth_path)
|
||||||
|
.oneshot(Request::builder().uri("/api/archives/test/entries/fake_uid/artifacts/0").body(Body::empty()).unwrap())
|
||||||
|
.await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn serve_artifact_with_auth_returns_ok() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let store_path = dir.path().join("store");
|
||||||
|
let paths = archivr_core::archive::initialize_archive(dir.path(), &store_path, "test", false).unwrap();
|
||||||
|
let artifact_relpath = "raw/a/u/page.html";
|
||||||
|
let artifact_dir = store_path.join("raw").join("a").join("u");
|
||||||
|
std::fs::create_dir_all(&artifact_dir).unwrap();
|
||||||
|
std::fs::write(artifact_dir.join("page.html"), b"<html>auth test</html>").unwrap();
|
||||||
|
let conn = database::open_or_initialize(&paths.archive_path).unwrap();
|
||||||
|
let user_id = database::ensure_default_user(&conn).unwrap();
|
||||||
|
let sid = database::upsert_source_identity(&conn, "web", "page", Some("auth-page"), Some("https://example.com/auth"), "https://example.com/auth").unwrap();
|
||||||
|
let run = database::create_archive_run(&conn, user_id, 1).unwrap();
|
||||||
|
let entry = database::create_archived_entry(&conn, &database::NewEntry {
|
||||||
|
source_identity_id: sid, archive_run_id: run.id, parent_entry_id: None, root_entry_id: None,
|
||||||
|
created_by_user_id: user_id, owned_by_user_id: user_id,
|
||||||
|
source_kind: "web".to_string(), entity_kind: "page".to_string(),
|
||||||
|
title: Some("Auth Test Page".to_string()), visibility: "private".to_string(),
|
||||||
|
representation_kind: "html".to_string(), source_metadata_json: "{}".to_string(), display_metadata_json: None,
|
||||||
|
}).unwrap();
|
||||||
|
let blob_id = database::upsert_blob(&conn, &database::BlobRecord {
|
||||||
|
sha256: "aaaa1111bbbb2222cccc3333dddd4444aaaa1111bbbb2222cccc3333dddd4444".to_string(),
|
||||||
|
byte_size: 21, mime_type: Some("text/html".to_string()), extension: Some("html".to_string()),
|
||||||
|
raw_relpath: artifact_relpath.to_string(),
|
||||||
|
}).unwrap();
|
||||||
|
database::add_entry_artifact(&conn, &database::NewArtifact {
|
||||||
|
entry_id: entry.id, artifact_role: "primary_media".to_string(),
|
||||||
|
storage_area: "raw".to_string(), relpath: artifact_relpath.to_string(),
|
||||||
|
blob_id: Some(blob_id), logical_path: None, metadata_json: None,
|
||||||
|
}).unwrap();
|
||||||
|
drop(conn);
|
||||||
|
let auth_path = dir.path().join("auth.sqlite");
|
||||||
|
{ let conn = archivr_core::database::open_auth_db(&auth_path).unwrap(); archivr_core::database::create_owner(&conn, "testowner", "dummy").unwrap(); }
|
||||||
|
let session_cookie = make_test_session(&auth_path);
|
||||||
|
let registry = ServerRegistry { archives: vec![MountedArchive { id: "test".to_string(), label: "Test".to_string(), archive_path: paths.archive_path.clone() }], bind: None, auth_db_path: None };
|
||||||
|
let uri = format!("/api/archives/test/entries/{}/artifacts/0", entry.entry_uid);
|
||||||
|
let response = app(registry, auth_path)
|
||||||
|
.oneshot(Request::builder().uri(&uri).header("cookie", &session_cookie).body(Body::empty()).unwrap())
|
||||||
|
.await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn serve_entry_favicon_requires_auth() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let (registry, _, auth_path) = make_test_registry(&dir);
|
||||||
|
let response = app(registry, auth_path)
|
||||||
|
.oneshot(Request::builder().uri("/api/archives/test/entries/fake_uid/favicon").body(Body::empty()).unwrap())
|
||||||
|
.await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn serve_entry_favicon_with_auth_returns_ok() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let store_path = dir.path().join("store");
|
||||||
|
let paths = archivr_core::archive::initialize_archive(dir.path(), &store_path, "test", false).unwrap();
|
||||||
|
let favicon_relpath = "raw/f/a/favicon.png";
|
||||||
|
let favicon_dir = store_path.join("raw").join("f").join("a");
|
||||||
|
std::fs::create_dir_all(&favicon_dir).unwrap();
|
||||||
|
std::fs::write(favicon_dir.join("favicon.png"), &[0x89u8, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]).unwrap();
|
||||||
|
let conn = database::open_or_initialize(&paths.archive_path).unwrap();
|
||||||
|
let user_id = database::ensure_default_user(&conn).unwrap();
|
||||||
|
let sid = database::upsert_source_identity(&conn, "web", "page", Some("fav-page"), Some("https://example.com/fav"), "https://example.com/fav").unwrap();
|
||||||
|
let run = database::create_archive_run(&conn, user_id, 1).unwrap();
|
||||||
|
let entry = database::create_archived_entry(&conn, &database::NewEntry {
|
||||||
|
source_identity_id: sid, archive_run_id: run.id, parent_entry_id: None, root_entry_id: None,
|
||||||
|
created_by_user_id: user_id, owned_by_user_id: user_id,
|
||||||
|
source_kind: "web".to_string(), entity_kind: "page".to_string(),
|
||||||
|
title: Some("Favicon Test".to_string()), visibility: "private".to_string(),
|
||||||
|
representation_kind: "html".to_string(), source_metadata_json: "{}".to_string(), display_metadata_json: None,
|
||||||
|
}).unwrap();
|
||||||
|
let blob_id = database::upsert_blob(&conn, &database::BlobRecord {
|
||||||
|
sha256: "ffffffffffff1111ffffffffffff1111ffffffffffff1111ffffffffffff1111".to_string(),
|
||||||
|
byte_size: 8, mime_type: Some("image/png".to_string()), extension: Some("png".to_string()),
|
||||||
|
raw_relpath: favicon_relpath.to_string(),
|
||||||
|
}).unwrap();
|
||||||
|
database::add_entry_artifact(&conn, &database::NewArtifact {
|
||||||
|
entry_id: entry.id, artifact_role: "favicon".to_string(),
|
||||||
|
storage_area: "raw".to_string(), relpath: favicon_relpath.to_string(),
|
||||||
|
blob_id: Some(blob_id), logical_path: None, metadata_json: None,
|
||||||
|
}).unwrap();
|
||||||
|
drop(conn);
|
||||||
|
let auth_path = dir.path().join("auth.sqlite");
|
||||||
|
{ let conn = archivr_core::database::open_auth_db(&auth_path).unwrap(); archivr_core::database::create_owner(&conn, "testowner", "dummy").unwrap(); }
|
||||||
|
let session_cookie = make_test_session(&auth_path);
|
||||||
|
let registry = ServerRegistry { archives: vec![MountedArchive { id: "test".to_string(), label: "Test".to_string(), archive_path: paths.archive_path.clone() }], bind: None, auth_db_path: None };
|
||||||
|
let uri = format!("/api/archives/test/entries/{}/favicon", entry.entry_uid);
|
||||||
|
let response = app(registry, auth_path)
|
||||||
|
.oneshot(Request::builder().uri(&uri).header("cookie", &session_cookie).body(Body::empty()).unwrap())
|
||||||
|
.await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn serve_blob_requires_auth() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let (registry, _, auth_path) = make_test_registry(&dir);
|
||||||
|
let sha256 = "0000000000000000000000000000000000000000000000000000000000000000";
|
||||||
|
let response = app(registry, auth_path)
|
||||||
|
.oneshot(Request::builder().uri(&format!("/api/archives/test/blobs/{sha256}")).body(Body::empty()).unwrap())
|
||||||
|
.await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn serve_blob_with_auth_returns_ok() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let store_path = dir.path().join("store");
|
||||||
|
let paths = archivr_core::archive::initialize_archive(dir.path(), &store_path, "test", false).unwrap();
|
||||||
|
let blob_relpath = "raw/b/l/data.bin";
|
||||||
|
let blob_dir = store_path.join("raw").join("b").join("l");
|
||||||
|
std::fs::create_dir_all(&blob_dir).unwrap();
|
||||||
|
std::fs::write(blob_dir.join("data.bin"), b"blob content here").unwrap();
|
||||||
|
let sha256 = "bbbb2222cccc4444bbbb2222cccc4444bbbb2222cccc4444bbbb2222cccc4444";
|
||||||
|
let conn = database::open_or_initialize(&paths.archive_path).unwrap();
|
||||||
|
database::upsert_blob(&conn, &database::BlobRecord {
|
||||||
|
sha256: sha256.to_string(), byte_size: 17,
|
||||||
|
mime_type: Some("application/octet-stream".to_string()), extension: Some("bin".to_string()),
|
||||||
|
raw_relpath: blob_relpath.to_string(),
|
||||||
|
}).unwrap();
|
||||||
|
drop(conn);
|
||||||
|
let auth_path = dir.path().join("auth.sqlite");
|
||||||
|
{ let conn = archivr_core::database::open_auth_db(&auth_path).unwrap(); archivr_core::database::create_owner(&conn, "testowner", "dummy").unwrap(); }
|
||||||
|
let session_cookie = make_test_session(&auth_path);
|
||||||
|
let registry = ServerRegistry { archives: vec![MountedArchive { id: "test".to_string(), label: "Test".to_string(), archive_path: paths.archive_path.clone() }], bind: None, auth_db_path: None };
|
||||||
|
let response = app(registry, auth_path)
|
||||||
|
.oneshot(Request::builder().uri(&format!("/api/archives/test/blobs/{sha256}")).header("cookie", &session_cookie).body(Body::empty()).unwrap())
|
||||||
|
.await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn list_tags_requires_auth() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let (registry, _, auth_path) = make_test_registry(&dir);
|
||||||
|
let response = app(registry, auth_path)
|
||||||
|
.oneshot(Request::builder().uri("/api/archives/test/tags").body(Body::empty()).unwrap())
|
||||||
|
.await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn list_tags_with_auth_returns_ok() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let (registry, _, auth_path) = make_test_registry(&dir);
|
||||||
|
let session_cookie = make_test_session(&auth_path);
|
||||||
|
let response = app(registry, auth_path)
|
||||||
|
.oneshot(Request::builder().uri("/api/archives/test/tags").header("cookie", &session_cookie).body(Body::empty()).unwrap())
|
||||||
|
.await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn list_entry_tags_requires_auth() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let (registry, _, auth_path) = make_test_registry(&dir);
|
||||||
|
let response = app(registry, auth_path)
|
||||||
|
.oneshot(Request::builder().uri("/api/archives/test/entries/fake_uid/tags").body(Body::empty()).unwrap())
|
||||||
|
.await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn list_entry_tags_with_auth_returns_ok() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let (registry, archive_path, auth_path) = make_test_registry(&dir);
|
||||||
|
let entry = make_test_entry(&archive_path);
|
||||||
|
let session_cookie = make_test_session(&auth_path);
|
||||||
|
let uri = format!("/api/archives/test/entries/{}/tags", entry.entry_uid);
|
||||||
|
let response = app(registry, auth_path)
|
||||||
|
.oneshot(Request::builder().uri(&uri).header("cookie", &session_cookie).body(Body::empty()).unwrap())
|
||||||
|
.await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn list_collections_requires_auth() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let (registry, _, auth_path) = make_test_registry(&dir);
|
||||||
|
let response = app(registry, auth_path)
|
||||||
|
.oneshot(Request::builder().uri("/api/archives/test/collections").body(Body::empty()).unwrap())
|
||||||
|
.await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn list_collections_with_auth_returns_ok() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let (registry, _, auth_path) = make_test_registry(&dir);
|
||||||
|
let session_cookie = make_test_session(&auth_path);
|
||||||
|
let response = app(registry, auth_path)
|
||||||
|
.oneshot(Request::builder().uri("/api/archives/test/collections").header("cookie", &session_cookie).body(Body::empty()).unwrap())
|
||||||
|
.await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn list_entry_collections_requires_auth() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let (registry, _, auth_path) = make_test_registry(&dir);
|
||||||
|
let response = app(registry, auth_path)
|
||||||
|
.oneshot(Request::builder().uri("/api/archives/test/entries/fake_uid/collections").body(Body::empty()).unwrap())
|
||||||
|
.await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn list_entry_collections_with_auth_returns_ok() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let (registry, archive_path, auth_path) = make_test_registry(&dir);
|
||||||
|
let entry = make_test_entry(&archive_path);
|
||||||
|
let session_cookie = make_test_session(&auth_path);
|
||||||
|
let uri = format!("/api/archives/test/entries/{}/collections", entry.entry_uid);
|
||||||
|
let response = app(registry, auth_path)
|
||||||
|
.oneshot(Request::builder().uri(&uri).header("cookie", &session_cookie).body(Body::empty()).unwrap())
|
||||||
|
.await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_collection_requires_auth() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let (registry, _, auth_path) = make_test_registry(&dir);
|
||||||
|
let response = app(registry, auth_path)
|
||||||
|
.oneshot(Request::builder().uri("/api/archives/test/collections/coll_notexist").body(Body::empty()).unwrap())
|
||||||
|
.await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_collection_with_auth_returns_ok() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let (registry, _, auth_path) = make_test_registry(&dir);
|
||||||
|
let session_cookie = make_test_session(&auth_path);
|
||||||
|
let create_resp = app(registry.clone(), auth_path.clone())
|
||||||
|
.oneshot(Request::builder().method("POST").uri("/api/archives/test/collections")
|
||||||
|
.header("content-type", "application/json").header("cookie", &session_cookie)
|
||||||
|
.body(json_body(&serde_json::json!({"name": "Auth Test Collection", "slug": "auth-test-coll", "default_visibility_bits": 2})))
|
||||||
|
.unwrap()).await.unwrap();
|
||||||
|
assert_eq!(create_resp.status(), StatusCode::CREATED);
|
||||||
|
let coll = body_json(create_resp).await;
|
||||||
|
let coll_uid = coll["collection_uid"].as_str().unwrap().to_string();
|
||||||
|
let uri = format!("/api/archives/test/collections/{coll_uid}");
|
||||||
|
let response = app(registry, auth_path)
|
||||||
|
.oneshot(Request::builder().uri(&uri).header("cookie", &session_cookie).body(Body::empty()).unwrap())
|
||||||
|
.await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue