Skip to content

Commit cbcdf76

Browse files
calliclesclaude
andauthored
Improve org access control and API response filtering (#8)
* Fix admin auth bypass: enforce GitHub org-admin checks in all environments Previously, authorizeOrgAccess() skipped all admin authorization in non-production environments, allowing any authenticated user to perform admin actions (update CLAs, toggle activation, manage bypass lists) and read admin panel data for any organization. Changes: - Remove blanket NODE_ENV !== "production" bypass from authorizeOrgAccess - Add scoped DB-admin fallback for non-production (only the designated installer passes, not any authenticated user), consistent with filterInstalledOrganizationsForAdmin - Restrict GET /api/sign/[orgSlug] to return only signing-relevant org fields (name, slug, avatar, isActive, claText) instead of the full org row which leaked adminUserId, installationId, etc. - Remove installedOrgsCount from GET /api/orgs response to prevent leaking total platform org count to non-admin users https://claude.ai/code/session_01FxG73b4jTBhGa5CK6kyKrG * Scope installedOrgsCount to user's authorized orgs Return the count of orgs the user is admin of rather than removing the field entirely, so the client can distinguish "no orgs installed" from "installed but no admin access". https://claude.ai/code/session_01FxG73b4jTBhGa5CK6kyKrG --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 50bd8ca commit cbcdf76

3 files changed

Lines changed: 17 additions & 10 deletions

File tree

app/api/orgs/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export async function GET() {
4141
return NextResponse.json({
4242
orgs,
4343
user: toSessionUserDto(user),
44-
installedOrgsCount: installedOrgs.length,
44+
installedOrgsCount: orgs.length,
4545
})
4646
} catch (err) {
4747
console.error("Failed to list authorized organizations:", err)

app/api/sign/[orgSlug]/route.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ export async function GET(
2828

2929
return NextResponse.json({
3030
org: {
31-
...org,
31+
name: org.name,
32+
githubOrgSlug: org.githubOrgSlug,
33+
avatarUrl: org.avatarUrl,
34+
isActive: org.isActive,
3235
claMarkdown: org.claText,
3336
},
3437
user: toSessionUserDto(user),

lib/server/org-access.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,21 @@ export async function authorizeOrgAccess(orgSlug: string): Promise<OrgAccessResu
3535
}
3636
}
3737

38-
if (process.env.NODE_ENV !== "production") {
39-
return { ok: true, org, user }
40-
}
41-
4238
try {
4339
const isAdmin = await isGitHubInstallationAccountAdmin(user, org)
4440
if (!isAdmin) {
45-
return {
46-
ok: false,
47-
status: 403,
48-
message: "Forbidden: GitHub installation admin access required",
41+
// In non-production, when no GitHub OAuth token is available for org-admin
42+
// checks, fall back to the DB admin mapping — consistent with
43+
// filterInstalledOrganizationsForAdmin. This is strictly scoped: only the
44+
// designated admin (who installed the app) passes, not any authenticated user.
45+
const hasDbAdminFallback =
46+
process.env.NODE_ENV !== "production" && org.adminUserId === user.id
47+
if (!hasDbAdminFallback) {
48+
return {
49+
ok: false,
50+
status: 403,
51+
message: "Forbidden: GitHub installation admin access required",
52+
}
4953
}
5054
}
5155
} catch (error) {

0 commit comments

Comments
 (0)