Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 131 additions & 4 deletions lib/api_projects/tests/project_key_auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,9 +282,8 @@ async fn project_key_list_plots() {
assert_eq!(resp.status(), StatusCode::OK);
}

// Negative: project key cannot access a different project
#[tokio::test]
async fn project_key_wrong_project() {
async fn project_key_for_other_project_on_public_target_returns_403() {
let server = TestServer::new().await;
let user = server.signup("Key User", "keywrong@example.com").await;
let org = server.create_org(&user, "Key Wrong Org").await;
Expand All @@ -310,6 +309,63 @@ async fn project_key_wrong_project() {
let key_created: JsonProjectKeyCreated = resp.json().await.expect("Failed to parse key");

// Try to access project B with project A's key
let resp = server
.client
.get(server.api_url(&format!("/v0/projects/{}/branches", slug_b)))
.header(
bencher_json::AUTHORIZATION,
bencher_json::bearer_header(key_created.key.as_ref()),
)
.send()
.await
.expect("Request failed");

assert_eq!(resp.status(), StatusCode::FORBIDDEN);
let body = resp.text().await.expect("Failed to read response body");
assert!(
body.contains("access denied"),
"Expected 'access denied' in body, got: {}",
body
);
}

#[cfg(feature = "plus")]
#[tokio::test]
async fn project_key_for_other_project_on_private_target_returns_404() {
use bencher_json::project::Visibility;
use bencher_schema::schema;
use diesel::{ExpressionMethods as _, QueryDsl as _, RunQueryDsl as _};

let server = TestServer::new().await;
let user = server.signup("Key User", "keywrongpriv@example.com").await;
let org = server.create_org(&user, "Key Wrong Priv Org").await;
let project_a = server.create_project(&user, &org, "Priv Project A").await;
let project_b = server.create_project(&user, &org, "Priv Project B").await;

{
let mut conn = server.db_conn();
diesel::update(schema::project::table.filter(schema::project::uuid.eq(project_b.uuid)))
.set(schema::project::visibility.eq(Visibility::Private))
.execute(&mut conn)
.expect("Failed to update project visibility");
}

let slug_a: &str = project_a.slug.as_ref();
let slug_b: &str = project_b.slug.as_ref();

let resp = server
.client
.post(server.api_url(&format!("/v0/projects/{}/keys", slug_a)))
.header(
bencher_json::AUTHORIZATION,
bencher_json::bearer_header(&user.token),
)
.json(&serde_json::json!({"name": "a-key"}))
.send()
.await
.expect("Failed to create key");
let key_created: JsonProjectKeyCreated = resp.json().await.expect("Failed to parse key");

let resp = server
.client
.get(server.api_url(&format!("/v0/projects/{}/branches", slug_b)))
Expand All @@ -322,6 +378,12 @@ async fn project_key_wrong_project() {
.expect("Request failed");

assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let body = resp.text().await.expect("Failed to read response body");
assert!(
body.contains("may be private"),
"Expected info-hiding wording in body, got: {}",
body
);
}

// Negative: project key cannot list keys (requires Manage permission)
Expand Down Expand Up @@ -530,7 +592,6 @@ async fn project_key_can_create_report() {
let _report: JsonReport = resp.json().await.expect("Failed to parse response");
}

// Negative: project key cannot create in wrong project
#[tokio::test]
async fn project_key_cannot_create_in_wrong_project() {
let server = TestServer::new().await;
Expand Down Expand Up @@ -571,7 +632,73 @@ async fn project_key_cannot_create_in_wrong_project() {
.await
.expect("Request failed");

assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
let body = resp.text().await.expect("Failed to read response body");
assert!(
body.contains("access denied"),
"Expected 'access denied' in body, got: {}",
body
);
}

#[cfg(feature = "plus")]
#[tokio::test]
async fn project_key_cannot_create_in_wrong_private_project_returns_404() {
use bencher_json::project::Visibility;
use bencher_schema::schema;
use diesel::{ExpressionMethods as _, QueryDsl as _, RunQueryDsl as _};

let server = TestServer::new().await;
let user = server
.signup("Key User", "keycrosscreatepriv@example.com")
.await;
let org = server.create_org(&user, "Cross Create Priv Org").await;
let project_a = server.create_project(&user, &org, "Priv Create A").await;
let project_b = server.create_project(&user, &org, "Priv Create B").await;

{
let mut conn = server.db_conn();
diesel::update(schema::project::table.filter(schema::project::uuid.eq(project_b.uuid)))
.set(schema::project::visibility.eq(Visibility::Private))
.execute(&mut conn)
.expect("Failed to update project visibility");
}

let slug_a: &str = project_a.slug.as_ref();
let slug_b: &str = project_b.slug.as_ref();

let resp = server
.client
.post(server.api_url(&format!("/v0/projects/{}/keys", slug_a)))
.header(
bencher_json::AUTHORIZATION,
bencher_json::bearer_header(&user.token),
)
.json(&serde_json::json!({"name": "cross-priv-key"}))
.send()
.await
.expect("Failed to create key");
let key_created: JsonProjectKeyCreated = resp.json().await.expect("Failed to parse key");

let resp = server
.client
.post(server.api_url(&format!("/v0/projects/{}/branches", slug_b)))
.header(
bencher_json::AUTHORIZATION,
bencher_json::bearer_header(key_created.key.as_ref()),
)
.json(&serde_json::json!({"name": "cross-priv-branch"}))
.send()
.await
.expect("Request failed");

assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let body = resp.text().await.expect("Failed to read response body");
assert!(
body.contains("may be private"),
"Expected info-hiding wording in body, got: {}",
body
);
}

// Negative: revoked project key is rejected
Expand Down
87 changes: 87 additions & 0 deletions lib/api_projects/tests/projects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,93 @@ async fn projects_hard_delete_nonexistent() {
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}

#[tokio::test]
async fn non_member_patch_public_project_returns_403() {
let server = TestServer::new().await;
let owner = server
.signup("Owner", "projpatchpubowner@example.com")
.await;
let outsider = server
.signup("Outsider", "projpatchpubother@example.com")
.await;
let org = server.create_org(&owner, "Patch Pub Org").await;
let project = server
.create_project(&owner, &org, "Patch Pub Project")
.await;

let project_slug: &str = project.slug.as_ref();
let body = serde_json::json!({ "name": "Hijacked" });
let resp = server
.client
.patch(server.api_url(&format!("/v0/projects/{project_slug}")))
.header(
bencher_json::AUTHORIZATION,
bencher_json::bearer_header(&outsider.token),
)
.json(&body)
.send()
.await
.expect("Request failed");

assert_eq!(resp.status(), StatusCode::FORBIDDEN);
let resp_body = resp.text().await.expect("Failed to read response body");
assert!(
resp_body.contains("access denied"),
"Expected 'access denied' in body, got: {}",
resp_body
);
}

#[cfg(feature = "plus")]
#[tokio::test]
async fn non_member_patch_private_project_returns_404() {
use bencher_json::project::Visibility;
use bencher_schema::schema;
use diesel::{ExpressionMethods as _, QueryDsl as _, RunQueryDsl as _};

let server = TestServer::new().await;
let owner = server
.signup("Owner", "projpatchprivowner@example.com")
.await;
let outsider = server
.signup("Outsider", "projpatchprivother@example.com")
.await;
let org = server.create_org(&owner, "Patch Priv Org").await;
let project = server
.create_project(&owner, &org, "Patch Priv Project")
.await;

{
let mut conn = server.db_conn();
diesel::update(schema::project::table.filter(schema::project::uuid.eq(project.uuid)))
.set(schema::project::visibility.eq(Visibility::Private))
.execute(&mut conn)
.expect("Failed to update project visibility");
}

let project_slug: &str = project.slug.as_ref();
let body = serde_json::json!({ "name": "Hijacked" });
let resp = server
.client
.patch(server.api_url(&format!("/v0/projects/{project_slug}")))
.header(
bencher_json::AUTHORIZATION,
bencher_json::bearer_header(&outsider.token),
)
.json(&body)
.send()
.await
.expect("Request failed");

assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let resp_body = resp.text().await.expect("Failed to read response body");
assert!(
resp_body.contains("may be private"),
"Expected info-hiding wording in body, got: {}",
resp_body
);
}

// Soft-delete project, verify child resource endpoints return 404
#[tokio::test]
async fn projects_soft_delete_endpoints_inaccessible() {
Expand Down
Loading
Loading