mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 02:44:50 +01:00
Protect Clip catalog integrity
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
c0b194ed4a
commit
e10d403e5e
2 files changed
+28
-8
No files matched your search
@@ -147,15 +147,15 @@ function publicClipFixture(input: { clipId: string; slug?: string }) {
|
||||
};
|
||||
}
|
||||
|
||||
function createApp() {
|
||||
function createApp(actor: Record<string, unknown> = {
|
||||
type: "board",
|
||||
source: "local_implicit",
|
||||
userId: "board",
|
||||
}) {
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use((req, _res, next) => {
|
||||
(req as any).actor = {
|
||||
type: "board",
|
||||
source: "local_implicit",
|
||||
userId: "board",
|
||||
};
|
||||
(req as any).actor = actor;
|
||||
next();
|
||||
});
|
||||
app.use("/api", clipRoutes({} as any));
|
||||
@@ -212,7 +212,7 @@ describe("clip routes", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects owner moderation and publish-status patches", async () => {
|
||||
it("rejects owner moderation, publish-status, and public-visibility patches", async () => {
|
||||
const clipId = "33333333-3333-4333-8333-333333333333";
|
||||
const sourceCompanyId = "22222222-2222-4222-8222-222222222222";
|
||||
mockClipService.getClipById.mockResolvedValue({
|
||||
@@ -228,12 +228,25 @@ describe("clip routes", () => {
|
||||
const publishRes = await request(createApp())
|
||||
.patch("/api/clips/" + clipId)
|
||||
.send({ status: "published" });
|
||||
const visibilityRes = await request(createApp())
|
||||
.patch("/api/clips/" + clipId)
|
||||
.send({ visibility: "public" });
|
||||
|
||||
expect(moderationRes.status).toBe(403);
|
||||
expect(publishRes.status).toBe(403);
|
||||
expect(visibilityRes.status).toBe(403);
|
||||
expect(mockClipService.updateClip).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects anonymous applied import telemetry", async () => {
|
||||
const res = await request(createApp({ type: "none", source: "none" }))
|
||||
.post("/api/public/clips/support-triage/import-telemetry")
|
||||
.send({ status: "applied" });
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
expect(mockClipService.recordImportTelemetry).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses the clip source company when building import previews", async () => {
|
||||
const destinationCompanyId = "11111111-1111-4111-8111-111111111111";
|
||||
const sourceCompanyId = "22222222-2222-4222-8222-222222222222";
|
||||
|
||||
@@ -643,6 +643,9 @@ export function clipRoutes(db: Db, storage?: StorageService) {
|
||||
if (req.body.status === "published" && clip.status !== "published") {
|
||||
throw forbidden("Publishing clips requires platform moderation access");
|
||||
}
|
||||
if (req.body.visibility === "public" && clip.visibility !== "public") {
|
||||
throw forbidden("Public clip visibility requires platform moderation access");
|
||||
}
|
||||
const actorInfo = getActorInfo(req);
|
||||
const updated = await svc.updateClip(clip.id, req.body, {
|
||||
actorType: actorInfo.actorType,
|
||||
@@ -751,7 +754,11 @@ export function clipRoutes(db: Db, storage?: StorageService) {
|
||||
}
|
||||
if (!consumeClipRateLimit(req, res, "import")) return;
|
||||
const actor = actorForClip(req);
|
||||
const clip = await svc.recordImportTelemetry(req.params.slug as string, req.body, actor);
|
||||
const telemetryStatus = req.body.status ?? "previewed";
|
||||
if (actor.actorType === "anonymous" && telemetryStatus !== "previewed") {
|
||||
throw forbidden("Authenticated actor required for applied import telemetry");
|
||||
}
|
||||
const clip = await svc.recordImportTelemetry(req.params.slug as string, { ...req.body, status: telemetryStatus }, actor);
|
||||
await logActivity(db, {
|
||||
companyId: req.body.destinationCompanyId ?? clip.sourceCompanyId,
|
||||
actorType: actor.actorType === "agent" ? "agent" : actor.actorType === "user" ? "user" : "system",
|
||||
|
||||
Reference in new issue
Block a user