Tighten shared project workspace runtime stop/restart auth

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-04-28 11:02:27 -05:00
1 parent 42e1b1bfa1
commit 647c2c487e
3 files changed
+64 -5

No files matched your search

@@ -320,6 +320,55 @@ describe.sequential("workspace runtime service route authorization", () => {
expect(mockAssertCanManageProjectWorkspaceRuntimeServices).toHaveBeenCalled();
}, 15000);
it("blocks shared-project stop/restart requests from agents", async () => {
mockProjectService.getById.mockResolvedValue(buildProject({
id: projectId,
workspaces: [{
id: workspaceId,
companyId: "company-1",
projectId,
name: "Workspace",
sourceType: "local_path",
cwd: "/tmp/project",
repoUrl: null,
repoRef: null,
defaultRef: null,
visibility: "default",
setupCommand: null,
cleanupCommand: null,
remoteProvider: null,
remoteWorkspaceRef: null,
sharedWorkspaceKey: "shared-key",
metadata: null,
runtimeConfig: null,
isPrimary: false,
runtimeServices: [],
createdAt: new Date(),
updatedAt: new Date(),
}],
}));
const app = await createProjectApp({
type: "agent",
agentId: "agent-1",
companyId: "company-1",
source: "agent_key",
runId: "run-1",
});
const responses = await Promise.all([
request(app).post(`/api/projects/${projectId}/workspaces/${workspaceId}/runtime-services/stop`).send({}),
request(app).post(`/api/projects/${projectId}/workspaces/${workspaceId}/runtime-services/restart`).send({}),
]);
for (const res of responses) {
expect(res.status).toBe(403);
expect(res.body.error).toContain("Missing permission");
expect(mockProjectService.getById).toHaveBeenCalledWith(projectId);
expect(mockAssertCanManageProjectWorkspaceRuntimeServices).not.toHaveBeenCalled();
}
}, 15000);
it("rejects agent callers that create project execution workspace commands", async () => {
const app = await createProjectApp({
type: "agent",
+13 -3
View File
@@ -14,7 +14,7 @@ import type { WorkspaceRuntimeDesiredState, WorkspaceRuntimeServiceStateMap } fr
import { trackProjectCreated } from "@paperclipai/shared/telemetry";
import { validate } from "../middleware/validate.js";
import { projectService, logActivity, workspaceOperationService } from "../services/index.js";
import { conflict } from "../errors.js";
import { conflict, forbidden } from "../errors.js";
import { assertCompanyAccess, getActorInfo } from "./authz.js";
import {
buildWorkspaceRuntimeDesiredStatePatch,
@@ -36,6 +36,7 @@ import { environmentService } from "../services/environments.js";
import { secretService } from "../services/secrets.js";
const WORKSPACE_CONTROL_OUTPUT_MAX_CHARS = 256 * 1024;
const SHARED_WORKSPACE_STOP_AND_RESTART_ACTIONS = new Set(["stop", "restart"]);
export function projectRoutes(db: Db) {
const router = Router();
@@ -346,6 +347,15 @@ export function projectRoutes(db: Db) {
return;
}
const isSharedWorkspace = Boolean(workspace.sharedWorkspaceKey);
if (
req.actor.type === "agent"
&& isSharedWorkspace
&& SHARED_WORKSPACE_STOP_AND_RESTART_ACTIONS.has(action)
) {
throw forbidden("Missing permission to manage workspace runtime services");
}
await assertCanManageProjectWorkspaceRuntimeServices(db, req, {
companyId: project.companyId,
projectWorkspaceId: workspace.id,
@@ -549,9 +559,9 @@ export function projectRoutes(db: Db) {
stderr,
system:
action === "stop"
? "Stopped project workspace runtime services.\n"
? "Stopped project workspace runtime services.\nThis does not pause issue work or held wake scheduling."
: action === "restart"
? "Restarted project workspace runtime services.\n"
? "Restarted project workspace runtime services.\nThis does not pause issue work or held wake scheduling."
: "Started project workspace runtime services.\n",
metadata: {
runtimeServiceCount,
+2 -2
View File
@@ -317,9 +317,9 @@ export function ProjectWorkspaceDetail() {
request.action === "run"
? "Workspace job completed."
: request.action === "stop"
? "Workspace service stopped."
? "Workspace service stopped. Issue execution is not paused."
: request.action === "restart"
? "Workspace service restarted."
? "Workspace service restarted. Issue execution is not paused."
: "Workspace service started.",
);
},