From bba8b7e8bbe3485039001d1244ee964f8a1a98c0 Mon Sep 17 00:00:00 2001 From: Raja-Hamid Date: Thu, 23 Jul 2026 20:34:45 +0500 Subject: [PATCH] fix(frontend): prevent editing description without write access The list row's editable input only means 'this is the private dashboard view' and is hardcoded to true by the container, so it was never an access check. A user with READ-only access to a shared workflow could therefore open the description editor; the save then failed with the backend's 'No sufficient access privilege.'. Gate description editing on the entry's actual access level: - add a canEditDescription getter (editable && entry.accessLevel === 'WRITE') - use it as the guard in onEditDescription() - hide the Edit Description control and the 'Write a description...' hover placeholder for read-only entries This mirrors the existing dataset list-item pattern (editable && entry.accessPrivilege === 'WRITE'). Closes #3497 --- .../user/list-item/list-item.component.html | 8 +- .../list-item/list-item.component.spec.ts | 74 ++++++++++++++++++- .../user/list-item/list-item.component.ts | 11 ++- 3 files changed, 88 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/dashboard/component/user/list-item/list-item.component.html b/frontend/src/app/dashboard/component/user/list-item/list-item.component.html index 20b98b71ac4..88a0c138519 100644 --- a/frontend/src/app/dashboard/component/user/list-item/list-item.component.html +++ b/frontend/src/app/dashboard/component/user/list-item/list-item.component.html @@ -74,7 +74,9 @@ -
+
diff --git a/frontend/src/app/dashboard/component/user/list-item/list-item.component.spec.ts b/frontend/src/app/dashboard/component/user/list-item/list-item.component.spec.ts index eca823a4ee7..44b3c4aa112 100644 --- a/frontend/src/app/dashboard/component/user/list-item/list-item.component.spec.ts +++ b/frontend/src/app/dashboard/component/user/list-item/list-item.component.spec.ts @@ -25,6 +25,7 @@ import { NzModalService } from "ng-zorro-antd/modal"; import { of, Subject, throwError } from "rxjs"; import { ActionType, HubService } from "../../../../hub/service/hub.service"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { By } from "@angular/platform-browser"; import { RouterTestingModule } from "@angular/router/testing"; import { StubUserService } from "../../../../common/service/user/stub-user.service"; import { UserService } from "../../../../common/service/user/user.service"; @@ -265,7 +266,12 @@ describe("ListItemComponent", () => { it("onEditDescription opens the edit modal and applies the change, then closes it", () => { component.editable = true; - component.entry = { id: 1, description: "old", type: "workflow" } as unknown as DashboardEntry; + component.entry = { + id: 1, + description: "old", + type: "workflow", + accessLevel: "WRITE", + } as unknown as DashboardEntry; const descriptionChange = new Subject(); const modalRef = { componentInstance: { descriptionChange }, destroy: vi.fn() }; const createSpy = vi.spyOn(modalService, "create").mockReturnValue(modalRef as any); @@ -288,6 +294,72 @@ describe("ListItemComponent", () => { expect(createSpy).not.toHaveBeenCalled(); }); + describe("read-only entries cannot edit the description (#3497)", () => { + // `editable` only means "private dashboard view" and is hardcoded true by the + // container, so the row itself must consult the entry's access level. + const makeEntry = (accessLevel: string) => + ({ + id: 1, + name: "wf", + description: "old", + type: "workflow", + accessLevel, + workflow: { isOwner: accessLevel === "WRITE", accessLevel }, + accessibleUserIds: [], + likeCount: 0, + viewCount: 0, + isLiked: false, + size: 0, + }) as unknown as DashboardEntry; + + const stubModal = () => + vi + .spyOn(modalService, "create") + .mockReturnValue({ + componentInstance: { descriptionChange: new Subject() }, + destroy: vi.fn(), + } as any); + + it("does not open the description editor without WRITE access", () => { + component.editable = true; + component.entry = makeEntry("READ"); + const createSpy = stubModal(); + + component.onEditDescription(); + + expect(createSpy).not.toHaveBeenCalled(); + expect(workflowPersistService.updateWorkflowDescription).not.toHaveBeenCalled(); + }); + + it("hides the Edit Description control for read-only entries", () => { + component.editable = true; + component.isPrivateSearch = true; + component.entry = makeEntry("READ"); + fixture.detectChanges(); + + expect(fixture.debugElement.query(By.css('button[title="Edit Description"]'))).toBeNull(); + }); + + it("still opens the description editor with WRITE access", () => { + component.editable = true; + component.entry = makeEntry("WRITE"); + const createSpy = stubModal(); + + component.onEditDescription(); + + expect(createSpy).toHaveBeenCalled(); + }); + + it("still renders the Edit Description control with WRITE access", () => { + component.editable = true; + component.isPrivateSearch = true; + component.entry = makeEntry("WRITE"); + fixture.detectChanges(); + + expect(fixture.debugElement.query(By.css('button[title="Edit Description"]'))).not.toBeNull(); + }); + }); + it("onCheckboxChange toggles the entry's checked flag and emits the change", () => { const entry = { checked: false } as unknown as DashboardEntry; const emitSpy = vi.fn(); diff --git a/frontend/src/app/dashboard/component/user/list-item/list-item.component.ts b/frontend/src/app/dashboard/component/user/list-item/list-item.component.ts index 5291b79b1d0..a6eb482779c 100644 --- a/frontend/src/app/dashboard/component/user/list-item/list-item.component.ts +++ b/frontend/src/app/dashboard/component/user/list-item/list-item.component.ts @@ -275,8 +275,17 @@ export class ListItemComponent implements OnChanges { }, 0); } + /** + * `editable` only means "this is the private dashboard view" and is hardcoded to true + * by the container, so it is not an access check. Editing the description additionally + * requires write access to this specific entry (#3497). + */ + get canEditDescription(): boolean { + return this.editable && this.entry.accessLevel === "WRITE"; + } + onEditDescription(): void { - if (!this.editable) return; + if (!this.canEditDescription) return; this.originalDescription = this.entry.description;