mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 00:53:38 +02:00
Tighten shared project workspace runtime stop/restart auth
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.",
|
||||
);
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user