Skip to content

fix(permissions): require explicit organization share for video downloads - #2052

Open
wasim-builds wants to merge 6 commits into
CapSoftware:mainfrom
wasim-builds:fix/download-permission-org-share
Open

fix(permissions): require explicit organization share for video downloads#2052
wasim-builds wants to merge 6 commits into
CapSoftware:mainfrom
wasim-builds:fix/download-permission-org-share

Conversation

@wasim-builds

@wasim-builds wasim-builds commented Jul 29, 2026

Copy link
Copy Markdown

Fixes #2037 by requiring an explicit sharedVideos entry for organization-level video download permission checks, aligning it with the view policy and space-level download checks.

Greptile Summary

This PR narrows organization-level video download access to explicitly shared organizations.

  • Removes implicit download authorization based solely on membership in the video's owning organization.
  • Skips the organization-membership query when the video has no explicit organization shares.
  • Adds an owner-access unit test.

Confidence Score: 4/5

The implementation appears safe to merge, with the non-blocking caveat that its central permission regression is not covered by the added test.

The authorization change implements the stated explicit-share requirement, but the only new test exits through the unchanged owner shortcut and would not detect a regression in organization-level checks.

Files Needing Attention: apps/web/tests/unit/video-download-permissions.test.ts

Important Files Changed

Filename Overview
apps/web/lib/video-download-permissions.ts Organization download authorization now checks only organizations explicitly associated through sharedVideos, matching the stated permission change.
apps/web/tests/unit/video-download-permissions.test.ts Adds owner-access coverage but does not execute or protect the changed organization-sharing behavior.
Prompt To Fix All With AI
### Issue 1
apps/web/__tests__/unit/video-download-permissions.test.ts:5
**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.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "fix(permissions): require explicit organ..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Context used:

@superagent-security superagent-security Bot added the contributor:flagged Contributor flagged for review by trust analysis. label Jul 29, 2026
@superagent-security

Copy link
Copy Markdown

🚨 Contributor flagged. Click here for more info: Superagent Dashboard

import { canUserDownloadVideo } from "@/lib/video-download-permissions";

describe("canUserDownloadVideo", () => {
it("grants download access to the video owner", async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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)

Prompt To Fix With AI
This 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!

.where(eq(sharedVideos.videoId, videoId));

const orgIds = [orgId, ...sharedOrgs.map((org) => org.organizationId)];
if (sharedOrgs.length > 0) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change makes the orgId arg effectively unused in this function. If the signature is meant to stay stable, consider aliasing it to _orgId (or dropping it entirely) so Biome doesn’t flag an unused variable and the API stays less confusing.

import { canUserDownloadVideo } from "@/lib/video-download-permissions";

describe("canUserDownloadVideo", () => {
it("grants download access to the video owner", async () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 shared_videos gating (member of org + explicit share => allow; member of org without share => deny), to prevent regressions.

Comment thread packages/sdk-recorder/src/index.ts Outdated
}
const set = this.listeners.get(event);
if (set) set.add(handler as EventHandler<keyof RecorderEventMap>);
if (set) set.add(handler as any);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using as any here drops a lot of type safety and can also make add/remove symmetry less obvious. You can keep the set typed and just normalize the handler once:

Suggested change
if (set) set.add(handler as any);
const set = this.listeners.get(event);
if (!set) return () => {};
const typedHandler = handler as EventHandler<keyof RecorderEventMap>;
set.add(typedHandler);
return () => {
set.delete(typedHandler);
};

db: () => ({
select: () => ({
from: (table: unknown) => ({
where: () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This db() mock ignores the where(...) conditions entirely, so the “explicitly shared org” test is really only asserting the sharedOrgs.length > 0 gating. If you want the test to actually protect the inArray(...orgIds) behavior, consider making the mock return org membership conditionally (or add a mismatch case where sharedVideos contains a different orgId and expect false).

Comment thread packages/sdk-recorder/src/index.ts Outdated
}
const set = this.listeners.get(event);
if (set) set.add(handler as EventHandler<keyof RecorderEventMap>);
const castHandler = handler as unknown as EventHandler<keyof RecorderEventMap>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the Set is always created right above, you can simplify this a bit and avoid the extra Map.get(...) in the unsubscribe closure:

Suggested change
const castHandler = handler as unknown as EventHandler<keyof RecorderEventMap>;
const set = this.listeners.get(event)!;
const castHandler = handler as unknown as EventHandler<keyof RecorderEventMap>;
set.add(castHandler);
return () => set.delete(castHandler);

} 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";

Copy link
Copy Markdown

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 at apps/web/lib/. The __tests__/unit/ directory is only two levels below apps/web/, so you'd need ../../lib/... for a relative path. Since @/lib is already aliased in both tsconfig.json and vitest.config.ts (and used by other unit tests like roles-permissions.test.ts), reverting to the alias is probably cleanest:

Suggested change
import { canUserDownloadVideo } from "../../../lib/video-download-permissions";
import { canUserDownloadVideo } from "@/lib/video-download-permissions";

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor:flagged Contributor flagged for review by trust analysis.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Download permission grants the owner's whole org, while view permission requires an explicit share

1 participant