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
5 changes: 5 additions & 0 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Hono, type Context } from "hono";

Check warning on line 1 in src/api/routes.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Items reference the same linked issue #734.

Check notice on line 1 in src/api/routes.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Open PR work references issue #734.

Check notice on line 1 in src/api/routes.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Issue discovery is disabled for this repo

This repo is configured for direct contribution review rather than issue-discovery flow.
import { z } from "zod";
import { analyzePRQueue, type AuthorRole, type ChecksStatus } from "../queue-intelligence";
import { completeGitHubWebOAuth, createSessionFromGitHubToken, pollGitHubDeviceFlow, startGitHubDeviceFlow, startGitHubWebOAuth } from "../auth/github-oauth";
Expand Down Expand Up @@ -4236,6 +4236,7 @@
if (isRepoFocusManifestPath(path)) return true;
if (isRepoAiConfigPath(path)) return true;
if (isRepoCheckBeforeStartPath(path)) return true;
if (isRepoValidateLinkedIssuePath(path)) return true;
if (isRepoContributorIssueDraftGeneratePath(path)) return true;
if (path === LINT_PR_TEXT_PATH || path === LINT_SLOP_RISK_PATH || path === LINT_ISSUE_SLOP_PATH) return true;
if (path === EXTENSION_PULL_CONTEXT_PATH && isExtensionScopedSession(identity)) return true;
Expand Down Expand Up @@ -4273,6 +4274,10 @@
return /^\/v1\/repos\/[^/]+\/[^/]+\/check-before-start$/.test(path);
}

function isRepoValidateLinkedIssuePath(path: string): boolean {
return /^\/v1\/repos\/[^/]+\/[^/]+\/validate-linked-issue$/.test(path);
}

function isIssueQualityPath(path: string): boolean {
return /^\/v1\/repos\/[^/]+\/[^/]+\/issue-quality$/.test(path);
}
Expand Down
22 changes: 22 additions & 0 deletions test/unit/access-boundary.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { afterEach, describe, expect, it, vi } from "vitest";

Check warning on line 1 in test/unit/access-boundary.test.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Items reference the same linked issue #734.

Check notice on line 1 in test/unit/access-boundary.test.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Open PR work references issue #734.

Check notice on line 1 in test/unit/access-boundary.test.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Issue discovery is disabled for this repo

This repo is configured for direct contribution review rather than issue-discovery flow.
import { createApp } from "../../src/api/routes";
import { createSessionForGitHubUser } from "../../src/auth/security";
import { upsertInstallation, upsertRepositoryFromGitHub } from "../../src/db/repositories";
Expand Down Expand Up @@ -51,6 +51,28 @@
expect(await other.json()).toMatchObject({ error: "forbidden_repo" });
});

it("a maintainer can REACH validate-linked-issue on their OWN repo, scoped per-repo (allowlist parity with check-before-start)", async () => {
const { app, env } = await setup();
const { token } = await createSessionForGitHubUser(env, { login: "alice", id: 101 });
const cookie = `gittensory_session=${token}`;
// Before the fix this returned 403 insufficient_role at the session allowlist (the route was omitted),
// even though the handler's requireSessionRepoAccess guard would admit a maintainer of their own repo.
const own = await app.request(
"/v1/repos/alice/repo-a/validate-linked-issue",
{ method: "POST", headers: { cookie }, body: JSON.stringify({ issueNumber: 1 }) },
env,
);
expect(own.status).toBe(200);
// The per-route guard still scopes: maintainer of A cannot validate against B.
const other = await app.request(
"/v1/repos/bob/repo-b/validate-linked-issue",
{ method: "POST", headers: { cookie }, body: JSON.stringify({ issueNumber: 1 }) },
env,
);
expect(other.status).toBe(403);
expect(await other.json()).toMatchObject({ error: "forbidden_repo" });
});

it("a pure miner (no maintainer role on any repo) cannot read ANY repo's maintainer settings", async () => {
const { app, env } = await setup();
const { token } = await createSessionForGitHubUser(env, { login: "miner-only", id: 900 });
Expand Down
Loading