-
Notifications
You must be signed in to change notification settings - Fork 1.7k
fix(permissions): require explicit organization share for video downloads #2052
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
1829509
ad473ee
06bba3c
f8359e6
7bb947d
ae291f6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| import { | ||
| organizationMembers, | ||
| sharedVideos, | ||
| spaceMembers, | ||
| spaceVideos, | ||
| } from "@cap/database/schema"; | ||
| import type { Organisation, User, Video } from "@cap/web-domain"; | ||
| import { beforeEach, describe, expect, it, vi } from "vitest"; | ||
| import { canUserDownloadVideo } from "../../../lib/video-download-permissions"; | ||
|
|
||
| let mockSharedOrgs: Array<{ organizationId: string }> = []; | ||
| let mockOrgMembers: Array<{ id: string }> = []; | ||
| let mockSharedSpaces: Array<{ spaceId: string }> = []; | ||
| let mockSpaceMembers: Array<{ id: string }> = []; | ||
|
|
||
| vi.mock("@cap/database", () => ({ | ||
| db: () => ({ | ||
| select: () => ({ | ||
| from: (table: unknown) => ({ | ||
| where: (condition: any) => { | ||
| let result: unknown[] = []; | ||
| if (table === sharedVideos) { | ||
| result = mockSharedOrgs; | ||
| } else if (table === organizationMembers) { | ||
| // Simple condition check for tests: if the where clause doesn't include the target orgs/users, return empty | ||
| const getCircularReplacer = () => { | ||
| const seen = new WeakSet(); | ||
| return (key: string, value: any) => { | ||
| if (typeof value === "object" && value !== null) { | ||
| if (seen.has(value)) return; | ||
| seen.add(value); | ||
| } | ||
| return value; | ||
| }; | ||
| }; | ||
| const conditionStr = JSON.stringify(condition, getCircularReplacer()) || ""; | ||
| if (mockOrgMembers.length > 0 && !conditionStr.includes("org-789")) { | ||
| result = []; | ||
| } else { | ||
| result = mockOrgMembers; | ||
| } | ||
| } else if (table === spaceVideos) { | ||
| result = mockSharedSpaces; | ||
| } else if (table === spaceMembers) { | ||
| result = mockSpaceMembers; | ||
| } | ||
| const promise = Promise.resolve(result); | ||
| (promise as Record<string, unknown>).limit = () => | ||
| Promise.resolve(result); | ||
| return promise; | ||
| }, | ||
| }), | ||
| }), | ||
| }), | ||
| })); | ||
|
|
||
| describe("canUserDownloadVideo", () => { | ||
| beforeEach(() => { | ||
| mockSharedOrgs = []; | ||
| mockOrgMembers = []; | ||
| mockSharedSpaces = []; | ||
| mockSpaceMembers = []; | ||
| }); | ||
|
|
||
| it("grants download access to the video owner", async () => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The sole test uses identical Knowledge Base Used: Web App (apps/web) Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/web/__tests__/unit/video-download-permissions.test.ts
Line: 5
Comment:
**Permission change remains untested**
The sole test uses identical `userId` and `ownerId` values, so it returns through the unchanged owner shortcut without exercising the new organization-sharing check. Add cases proving that an organization member is denied without a `sharedVideos` entry and allowed with one, otherwise the regression addressed by this PR can return without failing this suite.
**Knowledge Base Used:** [Web App (apps/web)](https://app.greptile.com/cap/-/custom-context/knowledge-base/capsoftware/cap/-/docs/web-app.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test currently only covers the owner short-circuit. Since this PR changes org-level download behavior, it’d be useful to add a case that exercises the new |
||
| const allowed = await canUserDownloadVideo({ | ||
| userId: "user-123" as User.UserId, | ||
| ownerId: "user-123" as User.UserId, | ||
| videoId: "vid-456" as Video.VideoId, | ||
| orgId: "org-789" as Organisation.OrganisationId, | ||
| }); | ||
|
|
||
| expect(allowed).toBe(true); | ||
| }); | ||
|
|
||
| it("denies download access to an org member when the video is not explicitly shared with the organization", async () => { | ||
| mockSharedOrgs = []; | ||
| mockOrgMembers = [{ id: "member-1" }]; | ||
| mockSharedSpaces = []; | ||
| mockSpaceMembers = []; | ||
|
|
||
| const allowed = await canUserDownloadVideo({ | ||
| userId: "user-123" as User.UserId, | ||
| ownerId: "user-456" as User.UserId, | ||
| videoId: "vid-789" as Video.VideoId, | ||
| orgId: "org-789" as Organisation.OrganisationId, | ||
| }); | ||
|
|
||
| expect(allowed).toBe(false); | ||
| }); | ||
|
|
||
| it("grants download access to an org member when the video is explicitly shared with the organization", async () => { | ||
| mockSharedOrgs = [{ organizationId: "org-789" }]; | ||
| mockOrgMembers = [{ id: "member-1" }]; | ||
| mockSharedSpaces = []; | ||
| mockSpaceMembers = []; | ||
|
|
||
| const allowed = await canUserDownloadVideo({ | ||
| userId: "user-123" as User.UserId, | ||
| ownerId: "user-456" as User.UserId, | ||
| videoId: "vid-789" as Video.VideoId, | ||
| orgId: "org-789" as Organisation.OrganisationId, | ||
| }); | ||
|
|
||
| expect(allowed).toBe(true); | ||
| }); | ||
|
|
||
| it("denies download access when the video is shared with a different organization", async () => { | ||
| mockSharedOrgs = [{ organizationId: "other-org-id" }]; | ||
| mockOrgMembers = [{ id: "member-1" }]; | ||
| mockSharedSpaces = []; | ||
| mockSpaceMembers = []; | ||
|
|
||
| const allowed = await canUserDownloadVideo({ | ||
| userId: "user-123" as User.UserId, | ||
| ownerId: "user-456" as User.UserId, | ||
| videoId: "vid-789" as Video.VideoId, | ||
| orgId: "org-789" as Organisation.OrganisationId, | ||
| }); | ||
|
|
||
| expect(allowed).toBe(false); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,7 +12,7 @@ export async function canUserDownloadVideo({ | |
| userId, | ||
| ownerId, | ||
| videoId, | ||
| orgId, | ||
| orgId: _orgId, | ||
| }: { | ||
| userId: User.UserId; | ||
| ownerId: User.UserId; | ||
|
|
@@ -26,20 +26,22 @@ export async function canUserDownloadVideo({ | |
| .from(sharedVideos) | ||
| .where(eq(sharedVideos.videoId, videoId)); | ||
|
|
||
| const orgIds = [orgId, ...sharedOrgs.map((org) => org.organizationId)]; | ||
| if (sharedOrgs.length > 0) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change makes the |
||
| const orgIds = sharedOrgs.map((org) => org.organizationId); | ||
|
|
||
| const [orgMembership] = await db() | ||
| .select({ id: organizationMembers.id }) | ||
| .from(organizationMembers) | ||
| .where( | ||
| and( | ||
| eq(organizationMembers.userId, userId), | ||
| inArray(organizationMembers.organizationId, orgIds), | ||
| ), | ||
| ) | ||
| .limit(1); | ||
| const [orgMembership] = await db() | ||
| .select({ id: organizationMembers.id }) | ||
| .from(organizationMembers) | ||
| .where( | ||
| and( | ||
| eq(organizationMembers.userId, userId), | ||
| inArray(organizationMembers.organizationId, orgIds), | ||
| ), | ||
| ) | ||
| .limit(1); | ||
|
|
||
| if (orgMembership) return true; | ||
| if (orgMembership) return true; | ||
| } | ||
|
|
||
| const sharedSpaces = await db() | ||
| .select({ spaceId: spaceVideos.spaceId }) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This relative path resolves to
apps/lib/video-download-permissions, which doesn't exist — the source file is atapps/web/lib/. The__tests__/unit/directory is only two levels belowapps/web/, so you'd need../../lib/...for a relative path. Since@/libis already aliased in bothtsconfig.jsonandvitest.config.ts(and used by other unit tests likeroles-permissions.test.ts), reverting to the alias is probably cleanest: